每年都会有人问我同一个问题:想入门CTF,但完全不会编程,到底应该从哪里开始?我的答案一直没变过:先学Python,然后在题目里学Python。CTF(Capture The Flag,夺旗赛)的核心是"在限时内解决一组安全挑战",而Python是这个圈子里使用频率最高的语言。不管是Web、密码学、杂项还是逆向、PWN,动手写脚本解题几乎是每天的日常,Python就像一把万能螺丝刀,什么场景都能拧两下。
这篇文章就是按这个思路写的——从零基础开始,把Python当成CTF的解题工具来学,而不是当成一门"课程"来背。内容覆盖环境搭建、语法速成、五大方向实战脚本、报错排查,最后给出一条能落地执行的进阶路线。希望你看完能做的第一件事,就是打开终端,开始跑第一个脚本。
1. CTF入门与Python的定位:为什么非它不可
1.1 CTF五大方向与Python的用武之地
很多新手第一次接触CTF,都会被五花八门的方向搞晕。其实常规赛事的题目就五大类,每一类都离不开编程能力,而Python几乎在每个方向都能出力:
| 方向 | 考察重点 | Python典型用途 |
|---|---|---|
| Web | SQL注入、XSS、命令执行、文件上传、反序列化等 | 构造HTTP请求、枚举参数、自动提取flag |
| Crypto | 古典密码、AES/RSA、哈希、随机数 | 快速实现加解密逻辑、大整数运算、破解弱密钥 |
| Misc | 编码转换、隐写术、流量分析、取证 | 处理文件头、解析图片、分析pcapng流量 |
| Reverse | 逆向工程、算法还原、注册机 | 编写模拟脚本、还原加密算法、与调试器配合 |
| PWN | 漏洞利用、栈溢出、堆利用 | 用pwntools编写exploit、收发数据、接管交互 |
你会发现,即使不做纯开发,CTF解题的每一个环节——从读题到拿flag——都需要写点东西。而Python语法简洁、库极其丰富,是"想法到脚本"距离最短的语言。
1.2 Python为什么是CTF圈子的"通用语"
不是C不好,也不是Go不优秀,但在CTF这种几个小时内要出结果的比赛里,Python有三个不可替代的优势。
第一,语法接近伪代码。你脑子里想的"先尝试所有可能的位移量,再用频率分析判断哪个是明文",几乎可以逐行翻译成Python,不需要被内存管理、指针、模板这些细节打断思路。
第二,生态太全了。requests处理HTTP,PIL处理图片,pwntools直接跟程序交互,pycryptodome和gmpy2覆盖加解密和大整数运算,scapy还能解析流量包。这些库一装,等于赛前就把工具箱备好了。
第三,胶水特性。很多时候解题不是纯Python搞定一切,而是用Python调起系统命令、处理临时文件、把工具链串起来。Python写这种"胶水脚本"是最顺手的。
一个现实判断:如果你准备参加三个月后的比赛,与其花时间研究语言特性,不如直接把Python放到题目里学。刷题碰到一个编码转换,就写一个转换脚本;碰到一道RSA,就写一个数字分解的脚本。很快你就会发现,语法慢慢熟了,解题思路也通了。
1.3 赛前环境准备:从安装到常用库一次配齐
先说Python安装。网上太多教程会让人去官网下载、一路点Next,然后……没有然后,因为环境变量没配,命令行里python直接报"不是内部或外部命令"。
我的建议是:装新版(3.8以上即可,不必追最新),安装时务必勾选"Add Python to PATH";装完打开命令行,敲python --version验证一下。如果你在Windows上遇到,也可以用微软商店直接搜Python安装,那个会自动处理好环境变量,省心很多。
python --version # 配置国内镜像加速,不然pip下载库会慢到怀疑人生 pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple编辑器推荐VS Code,安装Python插件后就能直接运行脚本。运行方式很简单:新建一个.py文件,右键"在终端中运行Python文件"。不需要配置复杂的调试器,先把东西跑起来最重要。
CTF常用的库一次装全:
pip install requests pycryptodome gmpy2 pwntools pillow pyzbar提示:Windows上如果gmpy2安装失败,不要硬扛编译报错,直接到Visual Studio Code或Python官网对应的wheel文件页下载匹配Python版本的whl,再pip install这个whl文件,一分钟搞定。
环境到位之后,接下来的所有脚本都是这个基础上跑的。
2. Python基础语法速成:按CTF需求学,不按教材学
2.1 先搞懂三件事:输入、输出与类型转换
CTF解题脚本大多长成一个样子:从题目给的参数出发,经过一系列处理,最后打印flag。所以输入输出和类型转换是最高频的操作。
# input()拿到的永远是字符串 s = input("> ") print(type(s)) # <class 'str'> # 需要当整数用时,必须显式转换 n = int(s) # 输出时,Python的print会自动换行,且多个参数间默认用空格隔开 print("flag", "is", "here")有一个点我会反复跟新人强调:eval这个函数能直接执行字符串里的表达式,网上很多代码示例喜欢用它来"简洁地"转数字或解析输入,但在CTF方向里,eval本身就经常是考点——它能让一段字符串变成代码执行,很多Web注入题考的就是你如何利用类似的入口。平时练习千万别图省事在自己的工具脚本里用eval,因为一旦脚本处理了不可信输入,后果很难预料。养成显式转换类型的习惯,后面会少踩很多坑。
另外要理解int(x, base)这个用法,处理十六进制、二进制字符串时非常常见:
print(int("ff", 16)) # 255 print(int("1010", 2)) # 102.2 字符串、字节与编码:CTF里的"文字游戏"基础
CTF题目的信息和flag,绝大多数都需要经过编码转换才能读出来。拿到一段看似乱码的字符串,实际上可能是十六进制、Base64、ASCII偏移、URL编码……这些都是Python里几行代码的事。
先搞清楚Python3里最折磨新手的一对概念:str和bytes。str是人能读的文本,bytes是计算机存的原生字节。两者不能直接拼接,中间靠encode/decode转换。
# 字符与ASCII码互转 print(ord('f')) # 102 print(chr(102)) # 'f' # 十六进制字符串转文本 bytes.fromhex("666c6167").decode() # 'flag' "flag".encode().hex() # '666c6167' # Base64解码 import base64 print(base64.b64decode("ZmxhZw==").decode()) # 'flag' # URL解码 from urllib.parse import unquote print(unquote("%66%6c%61%67")) # 'flag'理解这些转换,Misc方向的入门题就没那么可怕了。练得多了你会发现,"乱码"只是还没找到正确的解码方式而已。
2.3 循环与字典:写第一把"瑞士军刀"
循环解决的是"重复"问题。CTF里最常见的重复动作是枚举:枚举凯撒位移、枚举异或密钥、遍历参数。字典则用来做统计和映射,古典密码分析尤其依赖字符频率。
写一个字符频率统计脚本,是很多人的第一个"解题工具":
from collections import Counter cipher = "iqfcf{vFt1TsXs91_98X_6291jX9f3s9}" freq = Counter(cipher) print(freq.most_common(5)) # [('9', 4), ('f', 4), ('1', 3), ('X', 3), ...]再配一个循环,把凯撒位移全试一遍:
cipher = "iqfcf{vFt1TsXs91_98X_6291jX9f3s9}" for shift in range(26): result = "" for ch in cipher: if 'a' <= ch <= 'z': result += chr((ord(ch) - ord('a') + shift) % 26 + ord('a')) elif 'A' <= ch <= 'Z': result += chr((ord(ch) - ord('A') + shift) % 26 + ord('A')) else: result += ch if "flag" in result.lower(): print(shift, result)这个脚本用了两层循环:外层枚举位移,内层逐字符处理。类似这种"枚举+判断"的组合,在后面的RSA小指数攻击、简单异或解密里会反复出现。理解了它,你就理解了多数CTF脚本的基本骨架。
2.4 文件读写:处理二进制数据的基本功
Misc、Reverse方向经常要跟文件打交道。题目给一个图片、一个压缩包或者一个原始数据文件,脚本里第一步通常是打开、看文件头、按字节处理。
# 以二进制方式读取,才能看到文件的真实内容 with open("secret.png", "rb") as f: data = f.read() print(data[:8]) # b'\x89PNG\r\n\x1a\n',这是PNG的标准文件头判断文件真实类型时,文件头很关键。很多新手直接改后缀名打不开文件,先用十六进制看头部,再用Python判定真实类型:
def detect_type(data): if data[:8] == b'\x89PNG\r\n\x1a\n': return 'PNG' if data[:4] == b'\x89JF': return 'JPEG' if data[:2] == b'PK': return 'ZIP' return 'unknown'处理十六进制转储文件时,经常要把十六进制字符串还原为二进制文件:
with open("dump.hex", "r") as f: hex_str = f.read().strip().replace(" ", "").replace("\n", "") with open("out.bin", "wb") as g: g.write(bytes.fromhex(hex_str))文件操作的基本功,其实就是"以什么模式打开、读完是bytes还是str、写到哪里"。这三步想清楚,大多数跟文件有关的题就不会卡住。
3. 五大方向解题中的Python实战:从读题到拿flag
3.1 Crypto题实战:10行代码解一道入门RSA
密码学方向的题目是最适合练Python的,因为思路清晰、逻辑严密。拿一道入门级RSA题举例:题目给了大整数n、公钥指数e和密文c。解题思路是分解n得到两个质数p和q,算出欧拉函数,求出私钥指数d,再用pow(c, d, n)还原明文。
from sympy import factorint from gmpy2 import invert n = 90581 # 题目给的模数 e = 65537 # 公钥指数 c = 39167 # 密文 factors = list(factorint(n).keys()) p, q = factors[0], factors[1] print(f"p = {p}, q = {q}") phi = (p - 1) * (q - 1) d = int(invert(e, phi)) # 求e关于phi的逆元 m = pow(c, d, n) # 解密 # 把整数明文转成可见文本 flag = m.to_bytes((m.bit_length() + 7) // 8, 'big') print(flag)每一步的作用讲清楚:分解模数是RSA破解的关键环节;欧拉函数(p-1)(q-1)是计算私钥的基础;invert是求乘法逆元,相当于计算d;最后用Python内置的pow做模幂运算,这是大整数运算效率最高的方法,别用**运算符自己算模。
如果遇到更大的n,硬分解就跑不动了,思路会转向低加密指数攻击、共模攻击、padding预测等,但脚本骨架几乎不变:读参数、算密钥、解明文。这就是为什么我说,把一类题的脚本写熟,后面遇到变体能很快迁移思路。
3.2 Web题实战:用requests和正则自动提取flag
Web方向的核心是考察对漏洞原理的理解,但很多步骤需要用脚本完成。最典型的动作是:给一个URL,POST一个参数,循环试探,从返回内容里找flag。
import requests import re # 注意:这段脚本只在比赛平台/授权的靶场环境中使用 url = "http://target.example.com/query" for i in range(1, 50): data = {"id": f"{i}"} r = requests.post(url, data=data) if "flag" in r.text.lower(): print(f"第{i}条记录返回了关键内容") flag = re.search(r"flag\{[^}]+\}", r.text, re.I) if flag: print(f"Flag: {flag.group(0)}") break这里用到三个知识点。requests.post发送表单数据;r.text拿到响应文本;re.search用正则从大段HTML里提取flag格式的内容。CTF里flag一般有固定格式,比如常见的flag{...},正则写对就能精准捕获。
实战中经常遇到响应内容很大、直接肉眼找不过来,或者返回数据被编码的情况。这时候建议把每次响应的完整内容保存到本地文件,再慢慢分析,而不是每次都在脚本里打断点看输出。脚本里加一行:
with open(f"response_{i}.html", "w", encoding="utf-8") as f: f.write(r.text)这样既方便复盘,也避免响应太长把控制台刷爆。
注意:Web题一定要在题目指定靶场和授权范围内操作。CTF比赛是全新的、隔离的演练环境,和真实系统完全是两码事。
3.3 Misc题实战:从图片、二维码和流量包里挖信息
Misc在很多人眼里是"杂货铺",但高频套路其实就那么几类:编码转换、图片隐写、二维码处理、流量分析。Python在这些地方很顺手。
图片方向常常用到PIL。比如题给了一张图片,怀疑低比特位隐写了信息(LSB隐写),那就需要逐像素读取RGB值的最低位:
from PIL import Image im = Image.open("secret.png") width, height = im.size bits = "" text = "" for y in range(height): for x in range(width): r, g, b = im.getpixel((x, y))[:3] bits += str(r & 1) # 只取红色通道的最低位 if len(bits) == 8: ch = chr(int(bits, 2)) if 32 < ord(ch) < 127: # 只打印可见字符 text += ch bits = "" print(text)二维码处理更简单,直接调用库解码:
from pyzbar.pyzbar import decode from PIL import Image data = decode(Image.open("qr.png")) for item in data: print(item.data.decode())如果二维码上贴了遮挡物或背景不够干净,可以先用图像处理把二维码区域提出来再做二值化,再用这个库识别。这个技巧在做Misc题时很实用。
流量分析是另一个高频考点,尤其是热词里经常出现的pcapng和USB流量题。主流工具是Wireshark或者tshark,但Python里用scapy可以快速做批量过滤:
from scapy.all import rdpcap packets = rdpcap("capture.pcapng") for pkt in packets: if pkt.haslayer("Raw"): print(pkt.summary(), pkt.load)USB流量题实际上是在解析纯数据流,把USB HID键盘事件还原成按键。这类题用Python处理比手工翻Wireshark高效很多。核心思路是用tshark把usb相关包过滤出来,提取HID Data段的键值字节,再对照USB键盘映射表还原成字母。映射表网上能查到,把它存成一个字典,脚本跑完就是完整键盘输入记录。
这里有个小教训:拿到流量包先别急着写脚本,先抓几个关键包看字段结构,确认协议层次正确,再写解析逻辑。上来就套模板,很容易因为字段名不同白跑一遍。
3.4 逆向与PWN题里的"脚手架":pwntools入门
PWN方向是CTF里偏底层的方向,核心是写exp(exploit,利用脚本)和远程程序交互。pwntools是最常用的库,它的设计哲学就是"让脚本跟程序对话"。
from pwn import * # 远程连接题目给的地址和端口 io = remote("1.2.3.4", 10001) # 接收程序输出,直到某个固定字符串出现 io.recvuntil(b"Please input your name:") # 发送payload io.sendline(b"flag") # 持续交互,方便手动查看程序其他输出 io.interactive()这段代码虽然短,但完成了整个"连接、接收提示、发送数据、保持交互"的闭环。Reversing方向的题如果需要跟程序多次交互,或者需要根据反馈调整参数,pwntools这套交互框架也是首选。
不要一上来就啃堆利用、shellcode这些超难内容。先把pwntools的收发数据、交互流程跑通,能把题目跑起来,再逐步深入。工具的使用门槛,往往比想象中低很多。
4. 常见报错与解题脚本中的避坑经验
4.1 新手最容易翻车的五个Python报错
写解题脚本的过程,本质上是和报错打交道的过程。下面这些报错,基本是每个新人都会中招的,我把原因和解决办法整理成一个速查表:
| 报错信息 | 常见原因 | 解决方法 |
|---|---|---|
| ModuleNotFoundError | 库没安装 | pip install 对应库 |
| TypeError: can't concat str to bytes | 混用了str和bytes | 先统一调用encode/decode |
| UnicodeDecodeError | 以文本方式读二进制文件 | 改用"rb"模式,或指定encoding |
| ValueError: invalid literal for int() | 字符串里有空格或非数字字符 | 先strip()清理,再int()转换 |
| RecursionError | 递归太深 | 改用循环,或提高递归限制 |
处理UnicodeDecodeError是我个人踩过最多次的坑。很多流量导出或文件转储用文本方式打开后会报错,解决办法是明确按二进制读取,或者指定编码。判断标准很简单:文件内容里如果有大量非ASCII字符,就用"rb"。
4.2 一道Web题的启发:Python脚本为什么会"感觉好奇怪"
热词里有个很有意思的题目描述:"小小查询系统,输入id即可查询到信息,但是报错感觉好奇怪"。这道题反映的是Web方向里很经典的一类现象:输入正常参数返回正常内容,输入特殊内容却返回异常的报错信息,而这个报错本身就是解题线索。
很多新手第一次在Python脚本里复现这个过程时,会感觉"脚本怎么跟浏览器表现不一样",原因通常有三个。
第一个是编码问题。requests拿到响应后,自动猜测解码方式,但猜错时页面里的中文会乱码。解决方案是手动指定编码:r.encoding = "utf-8"。第二个是响应内容里有特殊字符,导致正则匹配失败。不要看到一段显示正常就直接用正则,先打印r.text的repr版本,看看是否有隐藏的换行、标签或不规则空格。第三个是会话状态问题。有些查询接口依赖Cookie或上一步的Token,直接用requests.post发一次请求会失败,需要用requests.session()保持状态。
这类"奇怪感觉"其实是很好的训练信号——每当脚本行为和预期不符,把现象拆解成"输入、输出、环境"三类,再逐项排查,比反复试错高效得多。
4.3 写解题脚本的六个好习惯
刷题多了以后,我总结了一套自己写脚本的习惯,分享出来:
- 每个脚本开头打印接收到的关键参数,确认和解说一致,再往下走。
- 把请求、解密、文件处理等动作拆成函数,复用时不用复制粘贴。
- 网络请求一律加超时和重试,题目服务器本来就容易卡顿。
- 响应和中间结果及时存文件,不只在控制台里看。
- 用tqdm给循环加进度条,大量枚举时心理压力小很多。
- 赛后把脚本精简、加注释,存进自己的exp_lib目录,按方向分类。
这六个习惯看起来简单,但能在比赛高压环境下省出大量时间。脚本不是写给自己爽的,是写给"几小时后的自己"看的——那时候你很可能已经忘了当时为什么这么写。
5. 持续进阶:从入门到常态化参赛的一条可行路径
5.1 一条清晰的学习路线
给零基础新人排一条不绕弯的学习路线,大概四个阶段:
| 阶段 | 目标 | 具体做法 |
|---|---|---|
| 第一阶段 | 能用Python处理文本和编码 | 拿一段Base64、hex、URL编码的字符串反复转换,直到看见种类编码不慌 |
| 第二阶段 | 能照葫芦画瓢写解题脚本 | 到CTF题库平台找Crypto和Misc方向的入门题,看题解,理解后自己复现 |
| 第三阶段 | 能独立分析题目并写完整exp | 做Web方向的模拟靶场,练习requests、正则、会话管理 |
| 第四阶段 | 按兴趣专精方向 | 喜欢Web就深入框架漏洞和绕过技巧,喜欢二进制就啃pwntools和底层利用 |
核心做法是,永远不要"先把Python学完再打CTF",两件事要并行。语法学到能写循环和函数,就可以开始找题目练手了。碰到不会的语法和函数,直接查官方文档,查完马上用到题里,记忆会非常牢固。
5.2 刷题平台、工具沉淀与合规实践
刷题平台我推荐几个自己常用的:BUUCTF题库量大,适合系统练;CTFshow有大量针对新人的低成本入门题;攻防世界适合按方向分类刷;polar CTF这类赛事官网也常有赛题归档,赛后可以拿来复盘。多平台交叉刷的好处是能接触不同出题风格,不会只熟悉某一家的偏好。
工具沉淀方面,很多选手会整理自己的"编码工具箱",比如圈子里常说的随波逐流CTF编码工具,就是把Base64、URL、进制转换、古典密码等常用编码解码能力集中到一起,比赛时当双保险用。但我始终建议,工具可以辅助,核心的转换逻辑一定要自己用Python写过一遍。因为比赛时网络环境、工具兼容性都不确定,自己的脚本永远不会"带不动"。
学有余力时,还可以关注行业内的一些合规实践,比如各类厂商的SRC漏洞报告渠道——在授权范围内,向厂商提交你发现的安全问题。这跟CTF比赛不一样,它面对的是真实系统,必须严格遵守规则和边界。我见过不少CTF选手通过这条路积累了宝贵的实战经验,但前提永远是合规和授权。
5.3 从单打独斗到常态化参赛的个人体会
参加常态化赛事以后,我发现一个规律:真正拉开差距的不是知识量,而是脚本沉淀速度和临场心态。比赛是几个小时的连续作战,临时去查库、查语法会让节奏断掉。所以我在比赛前一定会做两件事:检查常用库是否都装好,把过去整理的exp_lib目录拉到手边。
写题解(writeup)也是特别重要的一环。每次比赛后,把每道题的解题脚本浓缩成注释清晰的单文件,附上自己的踩坑记录,发到社区或个人博客。这个过程倒逼你理解题目本质,同时也让后来的新人少走弯路。我自己很多经验,都是从别人的writeup里学来的——这个圈子之所以高效,正是因为大家都愿意把经验写下来传下去。
我个人在带新人时反复强调的只有一句话:别把Python当一门"课"来学,把它当成一把"螺丝刀"。CTF里每一个需要你重复手工做的事,都值得写脚本;每一次报错,都是你精进工具的契机。从今天的环境搭建开始,到写出第一个能自动提取flag的脚本,这段路我走过,无数人也走过,它没有想象中那么难,但一定需要你亲手敲下第一行代码。