搞渗透测试这一行早晚都会撞上“看不懂”的报文。我第一次在某个模拟靶场测试后台登录框时明明在输入框里敲了一个单引号抓包出来却成了%27服务端回显里还有一堆#39;和\u0027当时我对着这些符号愣了很久。后来才发现这些不是乱码而是同一个字符在不同“语境”里的不同写法。编码在渗透测试里从来不是孤立的知识点它贯穿了请求发送、服务端解析、数据库查询、前端渲染一整条链路。你只有搞清楚编码在哪一层被解、在哪一层被查才能真正理解一个 payload 为什么命中、为什么失效。这篇文章就把我在实际测试里反复用到的那几种编码从头拆一遍内容包括 URL 编码、Base64、Hex、HTML 实体编码、Unicode/宽字节以及它们在 SQL 注入、XSS、命令注入里的具体作用。无论你是刚入门的新手还是已经写过不少 PoC 的测试者这几块东西都能帮你少踩几个坑。1. 为什么说编码是渗透测试绕不开的基础功很多新手会把“编码”当成一个单纯的知识点来背看到%20知道是空格看到YQ知道是 Base64。但真正做渗透测试时编码不是挂在墙上的字符表它就藏在每一条 HTTP 请求和响应里决定着你构造的 payload 能不能活着到达目标解析器。想理解这一点得先跳出一张表去看整条数据链。1.1 一次请求从客户端到数据库的“变形记”你在浏览器输入框里输入一个单引号浏览器不会直接把放进HTTP请求行里而是按 URL 规范把它编码成%27放进请求。服务端接住请求后框架会先把 URL 解码成再把参数值拿去处理。如果这个值最后被拼进 SQL 语句数据库收到的是真正的单引号于是查询语法被破坏这就产生了注入点。这中间每一步都有一层“可观察的手脚”。在Burp Suite里看到的是%27在PHP代码里$_GET[id]拿到的已经是到了数据库日志里又变成了闭合后的SQL片段。同一个字符在不同层有不同形态。你如果想绕过WAF就要找出WAF检查的层和解码的层之间的时间差你如果想定位注入点就要从报文里的编码形态反推出服务端到底解码了解几层。所以我一直建议新手做一件事拿到一个请求后先不要急着往Repeater里贴 payload先把原始报文从头到尾读一遍找出里面所有“不是明文”的片段再逐个问自己——这一层编码是谁产生的到哪个环节会被解开1.2 编码在渗透测试里的三个角色第一个角色是“格式转换器”。HTTP协议本身对不可打印字符、二进制内容、非 ASCII 字符并不友好所以请求参数里的中文、特殊符号要被转成 ASCII 可表示的形态。Base64 和 Hex 也是干这个的比如 HTTP Basic 认证里的Authorization: Basic Base64字符串本质上就是把用户名:密码这种二进制不友好的文本重新打包成协议允许的样子。第二个角色是“规则逃逸器”。WAF 和输入过滤大多是拿正则或关键字列表去匹配明文特征比如看见select、union、script就直接拦。如果把关键字编码一下比如把select写成se%6cect某些防护系统只检查一层就放行了结果服务端多解一层后又变回select。这就是编码对抗中最常见的手法也是很多人讨论“双重编码绕过”的底层原因。第三个角色是“分析线索”。从响应里看到\u4f60\u597d说明这是一个 JSON 接口里的中文被转成了 Unicode 转义看到PHNjcmlwdD4...说明某段数据可能经过 Base64 传输看到以eyJ开头的一长串字符大概率是 JWT。能读懂这些编码你就能快速判断一个系统的数据流、认证方式、序列化风格给后续测试省下大量试错时间。2. 高频编码逐个拆解这一章我按实际使用频率来排不讲太多理论推导重点放在“怎么认、怎么用、哪里会踩坑”上。2.1 URL 编码百分号编码URL 编码的规则很直白一个字节写成%加两位十六进制。空格是%20单引号是%27双引号是%22小于号是%3C大于号是%3E。原始字符如果不属于 URL 允许的保留字符集就会被这种形式替换。这里的十六进制数字大小写都不影响结果%3c和%3C是同一个字符。原字符URL编码用途联想空格%20参数内空格、命令注入里的分隔单引号%27SQL 注入闭合语句双引号%22HTML 属性注入%3CXSS 标签左尖括号%3EXSS 标签右尖括号#%23片段标识符防止截断%26参数分隔符本身需要编码%%25百分号自身的编码有一个细节经常让人迷糊HTML 表单的application/x-www-form-urlencoded中空格约定编码为而不是%20。在绝大多数框架里这两种写法最终都能被解析成空格但极少数实现只认其中一种。如果你遇到某个点用%20无效、换成却生效的情况不用太惊讶这是解析差异导致的不是玄学。实战中我在 SQL 注入里最常用到的就是%27、%20、%23组合。比如id1抓包出来是id1%27注释符号--后面还要再接一个空格或写成id1%27%20--%20这类形态。Burp Suite 的 Decoder 可以直接帮你做单层转换但你要分清报文里的%27是协议层编码WAF 检查时看到的通常是解码后形态还是解码前形态决定了这个编码有没有绕过意义。2.2 Base64 编码Base64 的本质是把二进制字节按 6 位一组重新映射到 64 个可打印字符。三个原始字节变成四个编码字符所以长度一定是 4 的倍数末尾可能出现或做填充。字符集只有A-Z a-z 0-9 /这 64 个加上填充用的特征非常明显。以字符串abc为例它的 ASCII 字节是01100001 01100010 01100011按 6 位切开是011000 010110 001001 100011对应查表就是Y、W、J、j所以编码结果是YWJj。这个计算过程不用死记理解“3字节变4字符”就够了因为判定特征比计算结果更重要。判断一段字符串是不是 Base64我会按顺序看三条字符集是否只在A-Za-z0-9/范围内长度是不是 4 的倍数末尾有没有填充。三条都满足才考虑它是 Base64。为什么要卡这么严因为很多业务数据本身就由大小写字母和数字组成比如订单号ABCD1234长度是 8满足 4 倍数也落在字符集里但它根本不是 Base64直接拿来解只会得到一堆乱码。渗透测试里 Base64 的高频出现位置包括HTTP Basic 认证头、表单里传输序列化数据、JSON 里嵌图片内容、JWT 的头部和载荷部分。测试时看到PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg这类字符串第一反应应该是解码看看内容是什么很可能就是一段被传输层编码包裹的 HTML 或脚本片段。2.3 Hex 编码与 ASCII 互转Hex 就是把一个字节写成两位十六进制比如单引号是0x27A是0x41a是0x61。这个编码在 Web 请求里不如 URL 编码常见但有两个经典场景非常值得掌握。第一个场景是数据库查询。MySQL 支持把十六进制字面量当字符串用SELECT * FROM users WHERE username 0x61646D696E等价于查admin。如果某处过滤了引号但没过滤数字和字母你就可以把字符串部分写成 Hex 绕过。注意这不是所有数据库都支持MySQL 可以SQL Server 就不吃这一套所以用之前要先确认目标库类型。第二个场景是 IP 和端口的花式写法。比如0x7F000001就是127.0.0.1如果你在测试 SSRF 或某些访问控制绕过时发现直接写 IP 被拦换成十六进制、十进制或混合进制形式有时能绕过基于字符串匹配的规则。这不算严格意义的编码但和 Hex 的知识直接相关我归到这一节一起记。ASCII 表不需要背完整记住三个起点就行字符0是0x30大写A是0x41小写a是0x61。这样遇到\x27、\x3C这类写法你能立刻还原出对应的字符。另外注意Hex 字符串的特征是只包含0-9和a-f并且长度一般是偶数。看到61646d696e这种一串字母数字先别急着当普通单词读它有可能是admin的 Hex 形态。2.4 HTML 实体编码HTML 实体编码有三种常见写法命名实体lt;代表、十进制数字实体#39;代表、十六进制数字实体#x27;代表。它们在 HTML 解析阶段会被浏览器还原成对应字符所以“编码后的字符”在页面上显示时看起来一切正常。这里有一个特别容易误解的地方我见过很多人搞混如果服务端把你输入的内容做了实体编码后输出在 HTML 文本节点里比如你提交scriptalert(1)/script响应里是lt;scriptgt;alert(1)lt;/scriptgt;浏览器会把这段内容当作纯文本渲染不会执行。原因是 HTML 解析器在遇到字符引用产生的时不会把它当作新标签的起始分隔符。换句话说标签定界符已经被实体编码“溶解”了。但实体编码并不总是安全的。如果用户输入被插入到 HTML 标签的某个属性中而且属性本身是可执行上下文情况就完全不同。比如构造一个onmouseover事件属性属性值里的#39;会在属性解析阶段被还原成单引号最终进入 JavaScript 引擎执行。所以看一个实体编码能不能防住 XSS不能只看“它编码了”还要看输出点在哪个上下文。HTML 文本、属性值、脚本代码、JSON 数据四种位置的解码规则和利用条件完全不一样。2.5 Unicode 转义与宽字节场景JSON 接口里最常见的编码形态是\uXXXX比如中文“好”会显示成\u597d。Python 里用json.loads会自动还原Burp Decoder 里也可以直接做 Unicode 解码。JWT 的载荷部分偶尔也会出现 Unicode 转义测试时看到\u开头基本可以确定是 JSON 或 JavaScript 字符串里的转义形式。还有一种老生常谈但依然值得理解的知识点是“宽字节注入”。早期的安全防护经常用转义函数在单引号前加反斜杠比如输入变成\在 ASCII 字节序列里就是0x5c 0x27。如果系统使用 GBK 这类宽字节编码与数据库通信0x5c可能和前面的高位字节组合成一个汉字后面的0x27就落单了转义失效单引号重新闭合出注入点。现在很多框架已经默认统一使用 UTF-8 并配合参数化查询这个场景不像以前那么普遍但在老系统、边缘硬件设备的内置 Web 服务里仍然可能碰到。这类问题的排查思路是先确认服务端字符集、数据库连接字符集、页面输出字符集三者是否一致。只要有一个环节是 GBK 而另一个是 UTF-8就存在解析歧义的可能不只是注入业务本身的乱码问题也常源于此。2.6 容易看走眼的变种形式除了上面五种基础形态还有几个变种我单独拿出来说一下因为它们经常干扰判断。第一是base64url。标准 Base64 使用/在 URL 参数里直接被当成特殊字符于是 JWT 等场景改用-和_替代并且通常去掉末尾的。所以看到以eyJ开头、中间有-和_的字符串不要用标准 Base64 解码器直接解先把-换回、_换回/再补填充位。第二是双重 URL 编码。%27再编码一次变成%2527。如果服务端有两次解码动作%2527就会先变成%27再变成。WAF 如果只解一次就会漏掉真正的单引号这就是双重编码绕过的原理。但实操中不要无脑叠层数服务端到底解几次是由中间件和框架决定的多试几次看响应变化才能确定。第三是畸形写法比如%u0027曾经在旧版 IIS 体系里被解析成单引号现代环境基本已经失效。测试时看到老设备、嵌入式设备的响应里出现这种编码还是值得一试但不能抱太大希望。3. 实战视角编码如何参与一次完整的测试过程前面拆的是知识单元这一章我把编码放回真实测试过程里看看它们在 SQL 注入、XSS、命令注入三类场景中是怎么发挥作用的。你会发现编码本身不产生漏洞它只是让 payload 绕过检测、适应解析链条的一种手段。3.1 场景一搜索框里的 SQL 注入与编码握手假设目标是个新闻搜索功能请求长这样GET /search?kwadmin HTTP/1.1 Host: target手动加一个单引号Burp Repeater 里抓到的是GET /search?kwadmin%27 HTTP/1.1如果服务端把请求参数 URL 解码后直接拼进 SQL最终执行的语句是SELECT * FROM news WHERE title LIKE %admin%末尾多出的单引号会破坏语法数据库抛错这就是注入信号。整个过程中%27起到了一个关键作用它让单引号能安全地通过 HTTP 传输层到达服务端后恢复原形再进入 SQL 拼接点。如果没有这一层编码浏览器和中间件会直接对裸引号报错或截断参数payload 根本走不到数据库那一层。绕过 WAF 的用法也一样。假设 WAF 拦截明文union测试者把语句里的union改写为uni%6fn。如果 WAF 只对原始报文做了一次 URL 解码检查它会看到union吗不会它只看到uni%6fn。但服务端如果解码后再拼接 SQL实际执行的已经是union。这就是编码在绕过场景中的核心价值。实操中我一直遵守一条原则先在 Repeater 里改原始报文发送不要复制浏览器地址栏里的 URL 去测。因为浏览器地址栏会做二次编码你看到的和实际发出的不是同一个东西。反复对比“改动前、改动后、响应变化”三个状态才能判断服务端到底吃不吃这一层编码。3.2 场景二XSS 中的 HTML 实体编码与解析顺序XSS 的输出点比 SQL 注入复杂得多因为最终解释你 payload 的是前端 HTML 解析器而 HTML 解析器对编码的处理有自己的一套顺序。最常见的失败案例某个评论功能把用户提交的scriptalert(1)/script中的尖括号做了 HTML 实体编码响应为plt;scriptgt;alert(1)lt;/scriptgt;/p浏览器解析时实体引用被还原为script文本但它只是文本节点不会再次进入标签解析流程。所以这个 XSS 尝试是失败的实体编码确实挡住了标签注入。但换一个输出点结果就不同了。假设用户输入被拼进一个标签属性img src用户输入 /输入 onerroralert(1)时没有加引号过滤的系统会直接闭合掉src注入出新的属性。如果过滤策略只是把单引号实体编码、双引号没处理攻击者完全可以用双引号闭合属性在事件属性里用实体编码后的单引号写 JS 字符串img srcx onerroralert(hit) /这里的#39;在 HTML 属性解析阶段被还原成再交给 JavaScript 引擎时已经是可用的字符串边界。实体编码不但没挡住攻击反而可能在 JavaScript 层帮你“修复”了引号冲突。所以测 XSS 时我拿到一个输入点会先做四件事确认输出位置在 HTML 标签内、属性内、脚本块内还是 JSON 数据里看服务端对 这五个字符分别做了什么处理看哪些字符没被处理可以作为注入锚点测的时候每次只改一个字符观察响应变化避免一次提交太多干扰判断。3.3 场景三命令注入里的传输层替代写法命令注入里最常见的是空格被过滤。目标如果直接把请求参数交给操作系统执行空格就是参数分隔符所以防护经常禁止空格出现。这时我会尝试用%09制表符代替空格因为很多 Shell 在解析命令时把制表符也当作分隔符。从编码角度看%09是 URL 编码形态的 Tab服务端解码后传给 ShellShell 并不区分空格和 Tab于是命令照常执行。另一种情况是管道符、分号、被过滤。写成 URL 编码形态%7C、%3B、%26是否能绕过完全取决于过滤发生的位置。如果过滤发生在 URL 解码之后那这些编码形态在过滤检查时已经被还原成符号了再编码也没用如果过滤发生在 URL 解码之前那么编码后就能跳过检查服务端解码后照常执行。这节内容顺带提醒一个常见误区很多人把${IFS}也当成编码其实它不是它是 Shell 内置变量由 Shell 展开成空白符。这类“特性替代写法”和编码经常混在一起用但原理不同。编码是字节形态的转换替代写法是解析器语法的等价变形。测试时两者都值得尝试但要能说清自己用的是哪一种。3.4 工具实操Burp Decoder 与 Python 脚本双视角Burp Suite 的 Decoder 面板是我最常用的编码处理工具。操作很简单在 HTTP 报文里选中可疑字符串右键选 Send to Decoder然后在 Decoder 里点 Encode 或 Decode 下拉框选择 URL、Base64、HTML、ASCII 等不同转换方式。面板左下角还有一个 Smart decode 选项它会对内容做启发式自动解码看到它一口咬定“这是 Base64”的时候别全信只把它当提示。我自己更多用 Python 跑解码脚本因为可以批量处理、保留中间结果。下面这个脚本可以循环剥离常见的几层编码同时打印每一层的结果方便看清解码链条import base64 import re import html import urllib.parse def decode_chain(s, max_round5): for i in range(max_round): print(fRound {i}: {s!r}) if re.search(r%[0-9A-Fa-f]{2}, s): s urllib.parse.unquote(s) continue if re.fullmatch(r[A-Za-z0-9/]{0,2}, s) and len(s) % 4 0 and len(s) 4: try: decoded base64.b64decode(s).decode(utf-8) if any(c.isprintable() for c in decoded): s decoded continue except Exception: pass if re.search(r(?:amp|lt|gt|quot|#\d|#x[0-9A-Fa-f]);, s): s html.unescape(s) continue if re.search(r\\u[0-9A-Fa-f]{4}, s): s s.encode(utf-8).decode(unicode_escape) continue break return s sample %2527%20OR%201%3D1--%20 print(Final:, decode_chain(sample))这个脚本的 Base64 判断条件里我加了一个限制先尝试把解码结果按 UTF-8 还原只有得到可打印文本才确认是 Base64。原因前面说过纯英文单词也可能满足 Base64 的字符集和长度特征不加这层校验会误判。实际测试时如果一段字符串经过四层解码还没有变成可读内容我基本会停止继续解转而去思考它是不是本来就是二进制数据或者有别的业务含义。4. 编码识别的快速判型与解码链路应对编码问题最快的路径是“先认类型再定解码顺序”。认错类型一切白搭定错顺序可能把 payload 搞得面目全非。我把平时判断用的外貌特征整理成了表格方便对着查。编码类型外貌特征示例URL 编码%后跟两位十六进制%27%3C%20Base644 的倍数、字符集限 A-Z a-z 0-9 /、末尾可出现 YWJjPHNjcmlwdD4Hex 字符串只含 0-9 与 a-f长度通常为偶数61646d696eadminHTML 实体以开头、分号结尾#39;lt;#x27;Unicode 转义\u后跟 4 位十六进制\u4f60base64url含-和_可能无eyJ...形式的 JWT4.1 一条实用的解码顺序我总结的解码顺序是先做一次 URL 解码再用 Base64 特征检查再看 HTML 实体和 Unicode 转义最后如果内容像压缩数据还要考虑先解压再解码。为什么把 URL 解码放在第一位因为 HTTP 请求里最外层最常见的传输编码就是 URL 编码它包裹了绝大多数实际数据。如果你在报文中看到一个%大概率是这一层的产物先剥掉它能让后续内容真正露出来。但要注意只做一层不要一上来就循环剥。很多合法业务参数里也可能包含%字符过度解码会把正常数据解坏。Base64 的检查放在第二层因为服务端逻辑经常在拿到参数后才做 Base64 解码这种包裹关系多出现在 URL 解码之后。HTML 实体和 Unicode 转义则多出现在响应数据里请求方向相对少。所以判断场景很重要你在请求里解编码和你在响应里解编码侧重点完全不同。遇到 JWT 时有个固定套路先按.切成三段第一、二段做 base64url 解码第三段是签名不去解。解码前把-换成、_换成/如果长度不是 4 的倍数就补。JWT 头里能看到alg字段载荷里能看到用户信息、过期时间等属于测试时最高频的编码分析对象。4.2 解码别贪多解到可读内容就停我见过不少新手拿到一串字符串就反复解码五六层非要把每个字节都变成明文才罢休。但很多时候数据本身就长这样。比如一个订单号存库前做了 Base64接口返回的就是T0QtMDAx你再往下解也得不到更深的语义。判断一句话解出来的内容如果已经能解释当前页面的行为就停。比如响应里的 Base64 字符串解出来是一段 JSON这段 JSON 正好对应页面上展示的数据那它就是业务数据本身不是攻击链的一环。只有当解码后的内容呈现出 SQL 关键字、可执行脚本、系统命令等“代码语义”时才值得继续分析。5. 常见问题与排查实录编码问题遇到多了你会发现很多现象其实是可以预测的。这一章我整理几个高频疑点都是实际测试中反复出现的。5.1 Base64 解码后全是乱码最常见的原因是原内容不是 UTF-8 文本。图片、压缩数据、加密密文、GBK 编码的中文都可能在 Base64 解码后显示成乱码。排查时先看原始数据的用途它出现在哪个接口、被前端怎么消费、响应头里有没有说明字符集。如果确定是文本就换编码再试raw base64.b64decode(encoded) for enc in [utf-8, gbk, latin-1]: try: print(raw.decode(enc)) except UnicodeDecodeError: continue如果所有常规编码都解不出来下一步考虑两种可能一是数据经过压缩需要先按 gzip 解压再解码二是数据本身就是随机字节根本不是给人读的。这两种情况我都遇到过别在第一层乱码上就放弃。5.2 解码两次之后内容反而变“坏”这种情况通常不是因为解错了而是因为原数据里天然包含类似编码的片段。举个例子一个合法的搜索词是100%25%E9%94%99它表示的其实是100%错其中%25就是百分号自身的编码。如果你看到%就做 URL 解码第一次得到100%错是正常的再解一次就会把原本正常的%拆成错误字符。另一个典型是 HTML 文本里出现amp;。amp;是的实体编码显示出来是。但如果你连续对整段内容做html.unescapeamp;lt;就会变成这可能会导致原本安全的文本被误判成 XSS payload。所以多层解码前先确认每一层编码是否真的被服务端或浏览器执行过而不是“看起来像”。5.3 同样的 payload 换个目标就不灵了这是最让人头疼的问题。同一个%2527绕过了上一个系统的 WAF这次却在第一个请求就被拦截或者干脆导致 500。原因往往在于服务端的解码层级、中间件类型、数据库连接字符集不同。排查思路是按“请求进来—参数解码—过滤检查—业务拼接—数据库执行—响应输出”这个链路一步步走。每遇到一个失败点就思考它卡在哪一层如果 WAF 拦了说明检查点看到了明文如果业务层收到的是%27而不是说明只解了一层如果 SQL 报错了但错误信息被吞掉说明注入点存在但出参被隐藏。多试几种可逆的编码组合URL 编码、双重 URL 编码、Hex、大小写变体结合每次响应差异就能反推出手法适用的具体环境。6. 一些实际经验判断最后分享几个我自己养成的“条件反射”希望能帮你少走弯路。第一看到一长串编码内容不要急着解先分清它是“协议层编码”还是“业务层自定义编码”。协议层编码是浏览器和中间件自动加上的比如%20、%27它们不改变参数的真实语义业务层编码是应用自己实现的比如把 JSON 数据 Base64 后再塞进参数它才是测试时真正要关注的重点。第二条件反射式地记住几个特征串eyJ开头是 JWT末尾带的 4 倍数大小写混合字符串大概率是 Base64%u开头是旧式 Unicode 编码现代系统很少认#x开头是 HTML 十六进制实体。这些特征能帮你在抓包后一分钟内锁定方向而不是逐个编码方式去试。第三解码时保留中间态。我做多层解码一定会在每一步打印当前结果或记录到文件里因为最后解出来的最终值可能仍然不可读但中间某一步已经能看出业务语义了。没有中间态你就不知道是哪一层出了问题。第四不要迷信“编码一定可以绕过”。编码能绕的是“只做字符串匹配”的过滤绕不过“解码后再匹配”的防护也绕不过参数化查询。真正有价值的不是编码技巧本身而是你判断“服务端在哪一层做了什么处理”的能力。这个能力只能靠多看报文、多对比响应来积累没有任何捷径。
阅读完成 · 觉得有帮助?