2019年秋招季,我在一所普通211的机房点开了奇安信2019校招笔试题(二)。说实话,题量不算大,但风格让我印象很深——没有太多纯算法题,也没有“手写单例模式”这种套路化问题,取而代之的是大量“给你一段代码,请指出漏洞并修复”的场景题。后来我才慢慢想明白,这才是安全公司校招笔试应该有的样子:它不care你会背多少知识点,它想看的是你在面对一段不可信代码时,会不会下意识站在攻击者角度做判断。
这篇文章不是原题答案速查,而是站在一个过来人的视角,复盘那套题里真正拉分的几个考点:路径遍历、密码学配置、编程题写法,以及考完后用什么方式把知识漏洞补上。如果你正在准备奇安信、深信服、绿盟这类安全公司的校招,或者想从开发岗转安全岗,这篇应该能帮你省下不少瞎摸索的时间。
1. 那套卷子的隐藏主线:从“会不会”到“怎么想”
先说结论:这套笔试题(二)和互联网大厂开发岗笔试最大的分水岭,不是考察范围,而是考察粒度。开发岗可能会问你“HashMap和Hashtable有什么区别”,安全岗则会问“如果用户输入的key会拼进一个日志文件,你怎么写才不产生日志注入”。同一个知识点,开发岗考概念,安全岗考风险。这是整个卷子给我的第一观感。
我凭记忆把卷子大致分成四块:网络与操作系统基础、Web安全、密码学、算法编程。虽然具体分值不一定记得准,但分布趋势大概是这样的:
| 模块 | 大概占比 | 代表性考点 |
|---|---|---|
| 网络与操作系统基础 | 20% | TCP三次握手状态、HTTP状态码语义、Linux文件权限 |
| Web安全 | 35% | SQL注入、XSS、CSRF、路径遍历、反序列化 |
| 密码学基础 | 20% | RSA简单计算、AES模式辨析、密码哈希存储 |
| 算法/编程题 | 25% | 字符串/数组处理、边界条件设计 |
这四块不是孤立存在的。奇安信作为安全公司,笔试题尤其喜欢在“Web安全”和“编程题”之间埋交叉考点:同样是给一段代码找问题,开发岗希望你回答“这里会抛空指针”,安全岗希望你回答“这里用户可控输入可能导致任意文件读取”。这两种回答之间的差距,就是“会不会写代码”和“能不能做安全开发”的差距。
1.1 安全岗笔试和开发岗笔试的本质区别
举一个我在卷子里印象很深的例子:一个文件下载接口,代码大概长这样:
String fileName = request.getParameter("fileName"); File file = new File("/data/files/" + fileName); response.setContentType("application/octet-stream"); // 读取file并返回给客户端开发岗视角是:文件名从参数来,拼到路径里,逻辑很顺,没什么问题。安全岗视角则是:fileName里如果带../呢?如果带URL编码呢?如果用户传一个绝对路径呢?同样一段代码,前者看到功能,后者看到风险。这就是安全思维的第一层:永远假设外部输入不可信。
第二层差异是影响面分析。开发岗答完“这里有bug”可能就结束了,安全岗还要继续想:这个漏洞能不能远程利用?能不能读取配置文件?能不能进一步升级成任意文件写入?在笔试卷上,哪怕题目只要求“指出漏洞”,我也会在答案末尾补一句“攻击者可能利用该接口读取系统用户文件或应用配置,造成敏感信息泄露”。这不仅是展示知识面,更是在告诉阅卷人:我知道这个漏洞的危害边界在哪里。
第三层是修复成本。安全岗笔试里最常见的“给你五分钟写修复方案”,考察的不是你能不能写出最完美的代码,而是你有没有考虑兼容性和误用风险。比如路径遍历,最简单的写法是:
if (fileName.contains("../")) { throw new SecurityException("invalid path"); }但这种黑名单方案遇到编码绕过就废了。更好的方式是先做路径规范化,再校验最终路径是否还在允许目录内。在笔试现场能写出第二种方案的考生,大概率见过真实漏洞,而不只是背过理论。
1.2 我在考场上总结的答题策略:先啃简答,再做选择
我见过很多同学按题目顺序一路做,结果在单选多选上抠了半小时,最后编程题没时间写,简答题随便糊两行。这套卷子不是按难度排的,前面的选择题也可能很阴间。我的策略是:发卷后先花两分钟把简答题扫一遍,优先写“逻辑清晰、能拿大分”的题目。
为什么?因为简答题按点给分,你写一个修复方向就有分;多选题错一个选项就全废,性价比太低。假设简答题里有路径遍历、RSA计算、编程题,我会先做RSA计算和路径遍历修复,这两道题逻辑确定,10分钟内能拿满。然后回头处理概念型选择,比如“HTTP方法安全”“SQL注入绕过”这类,遇到不确定的先标记,大题做完再检查。
如果笔试系统支持Markdown,我会在简答题文本框中用清晰的小标题把“漏洞描述、利用场景、修复方案、测试用例”分开。阅卷人一天要看很多份卷子,结构清爽的答案天然有印象分。这个习惯我到现在做安全评审时还在用,算是那场笔试留下的一个正向副作用。
2. 路径遍历这道综合题:从文件下载接口到目录穿越的完整分析
这套卷子里,我印象最深的是路径遍历综合题。它不是CTF里那种偏门考点,而是非常贴近真实业务的一个“文件下载”接口。在线答题页给了一段简化后的Java代码,描述大概如下:
public void download(HttpServletRequest request, HttpServletResponse response) { String fileName = request.getParameter("fileName"); String basePath = "/data/files/"; File file = new File(basePath + fileName); // 读取file并输出到response }题目有三问:第一,指出这段代码存在的安全漏洞;第二,攻击者可能利用这个漏洞做什么;第三,给出修复方案。这种题在安全公司笔试题里很经典,因为它把“攻击原理”和“代码修复”放进了同一个场景,天然适合考察综合能力。
我当时的思路是:不要只说“文件名没校验”,要说明攻击者如何把路径一层层往上带。正常请求是download?fileName=report.pdf,读取的是/data/files/report.pdf。但如果用户传入download?fileName=../../../../etc/passwd,在Linux环境下,系统会从/data/files/出发,往上层一级一级跳,最后落到/etc/passwd。这还不是最可怕的,如果这个接口换成上传或者保存文件,同样的逻辑可能变成任意文件写入,危害直接升级。
2.1 漏洞核心:目录拼接不等于路径约束
很多同学写过new File(basePath + fileName),觉得前缀目录写死了,路径就一定被约束在目录里。实际上,basePath + fileName只是在字符串层面做了拼接,并没有在路径语义层面做约束。操作系统解析路径时,..就是“上一级目录”,它不会因为你前面加了/data/files/就自动禁止跳出去。
这里有一个容易被忽略的细节:Web容器拿到URL参数时,可能已经做过一次URL解码。攻击者把../写成%2e%2e%2f,有些框架会解码成../,有些不会。如果应用里还有多个过滤器,参数的编码状态可能在多层之间不断变化。所以修复时不能只看当前代码,还要考虑整个请求链路对参数做了什么处理。我在答题时专门写了一句:“建议在入口处统一做输入规范化和校验”,后来工作里做代码审计,发现这句话确实是对的方向。
另外,Windows环境比Linux更麻烦。Linux下主要防御../、绝对路径、符号链接,Windows还有盘符、UNC路径、保留设备名、短文件名等一堆边缘情况。笔试不会要求你把所有平台差异写全,但至少要有“路径问题要考虑跨平台”的意识。能写出这句,阅卷人会认为你见过真实代码,而不是只会背例题。
2.2 怎样把“利用与修复”写到阅卷人愿意给满分
我当时把答案组织成四段,你也可以直接照这个结构来:
- 漏洞类型:路径遍历(Path Traversal / Directory Traversal),对应CWE-22。
- 攻击方式:通过修改
fileName参数,使用../或编码绕过,读取任意文件。 - 影响范围:读取系统敏感文件、应用源码、数据库配置,造成信息泄露。
- 修复方案:使用
Path.normalize()做规范化,再用startsWith()做目录约束。
修复代码我写的是这样一个版本:
String fileName = request.getParameter("fileName"); Path baseDir = Paths.get("/data/files/").toAbsolutePath().normalize(); Path target = baseDir.resolve(fileName).toAbsolutePath().normalize(); if (!target.startsWith(baseDir)) { throw new SecurityException("非法文件路径"); } File file = target.toFile(); if (!file.exists() || !file.isFile()) { throw new FileNotFoundException("文件不存在"); } // 继续读取文件并返回这段代码每一行都有作用:第二行先把允许访问的根目录变成标准形式;第三行把用户传入的文件名resolve到根目录下,再做一次normalize,把../都归并掉;第四行检查目标路径是否仍然以根目录开头,如果用户传了../../../../etc/passwd,normalize之后的路径就不再以baseDir开头,直接拒绝;最后再判断文件是否存在,并限制只能读取普通文件,避免命中目录或特殊文件。
在答案里我额外写了几组测试用例,这是很多应届生容易漏掉的部分:
- 正常请求:
fileName=report.pdf,期望返回文件。 - 恶意请求:
fileName=../../../../etc/passwd,期望返回错误。 - 编码请求:
fileName=%2e%2e%2f%2e%2e%2fetc/passwd,期望返回错误。 - 绝对路径:
fileName=/data/secret.txt,期望返回错误。
写上测试用例,阅卷人会默认你有测试意识和交付意识。安全岗笔试不只是看你有没有漏洞嗅觉,还看你会不会把方案落地成可以验证的东西。
3. 密码学模块:RSA参数、AES模式,以及“概念迁移”的拿分技巧
很多人做安全岗笔试题,最怕密码学。其实奇安信这类公司的校招笔试题不会让你破解现代加密,更多是看基础概念清不清楚,以及能不能从“会用”上升到“会选”。我记得那套卷子里有一道RSA小计算,题干类似“已知n=33,e=7,d=3,明文m=5,求密文c”。这种题放在实际场景里太小了,但用来验证考生是否理解模幂公式,非常有效。
3.1 一道RSA小计算题的完整推导
看到这种题,别慌。RSA公钥加密公式就一条:c = m^e mod n。把数字代进去就是c = 5^7 mod 33。手算的时候,千万不要真的把5自乘7次再取模,那不仅容易错,还很浪费时间。我当时是这么拆的:
先算5^2 = 25,再算5^4 = 25^2 = 625,而625 mod 33 = 625 - 33×18 = 625 - 594 = 31。于是:
5^7 = 5^4 × 5^2 × 5 = 31 × 25 × 5 = 3875
3875 mod 33,因为33×117 = 3861,所以3875 - 3861 = 14。密文c = 14。
如果想验证一下私钥解密,用m = c^d mod n:14^3 = 2744,33×83 = 2739,余数是5,正好还原明文。这种题把公式和中间步骤写清楚,基本就拿满了。
| 步骤 | 公式 | 结果 |
|---|---|---|
| 加密 | c = 5^7 mod 33 | 14 |
| 解密 | m = 14^3 mod 33 | 5 |
这里有一个关键点:RSA计算公式是模幂,不是直接求幂。很多同学写到最后忘记mod n,或者把e和d用反,那是必扣分的。还有,如果题目再延伸问φ(n)、公钥和私钥的数学关系,也要能够解释φ(n) = (p-1)(q-1)这个来源。笔试里不一定会让你硬算大数,但基本功不能模糊。
3.2 比计算更容易失分的AES模式辨析
除了RSA计算,密码学简答题还特别爱问:“AES-ECB模式有什么问题?为什么推荐GCM?”这个问题看似在考加密算法,实际在考安全配置能力。
ECB模式最大的问题,是同样的明文块会被加密成同样的密文块。如果加密一张图片,图片轮廓会在密文里隐隐可见;如果攻击者能让受害者加密自己构造的明文块,还能通过密文是否相同判断出“两个明文块是否相同”,这就产生了模式泄露。所以在安全场景里,ECB通常是不被推荐的。
但如果你只说“ECB不安全,所以用CBC”,还没答到点子上。CBC虽然解决了模式泄露,但不提供完整性保护。更稳妥的答法是:推荐使用GCM这类带认证的AEAD模式,因为它同时提供机密性、完整性、以及防重放能力。把这三个关键词写上去,阅卷人就知道你不是死记硬背。
还有一类常见的选择题:“密码存储应该用MD5还是SHA-256?”很多应届生会选SHA-256,但实际上,单纯SHA-256在GPU暴力破解面前依然不够。正确的思路是选加盐的慢哈希,比如bcrypt、scrypt、PBKDF2。把“哈希算法”和“密码存储”这两个概念分开,是安全岗的基本功。把这些内容放在一起记,密码学部分基本不会失分。
4. 编程题:一道“第一个不重复字符”,怎样写出安全岗风格
笔试题(二)里的编程题没有LeetCode的Hard题,我记得有一道和字符串有关的题,类似“给定一个字符串,找到第一个不重复的字符并返回下标,如果不存在返回-1”。这道题看起来简单,但安全公司的核分点有点不一样。除了要A,还要A得像个安全工程师。
4.1 我的第一版代码与第二版代码的差别
第一版我写得非常快,就是标准的哈希计数法:
def first_unique_char(s): dic = {} for ch in s: dic[ch] = dic.get(ch, 0) + 1 for i, ch in enumerate(s): if dic[ch] == 1: return i return -1这个版本功能上没有大问题,时间复杂度O(n),空间复杂度O(字符集大小),能过大部分测试点。但放到安全岗的语境下,我会再补一层“对不可信输入保持怀疑”的处理:
def first_unique_char(s): if not isinstance(s, str) or len(s) == 0: return -1 if len(s) > 10**6: return -1 count = {} for ch in s: count[ch] = count.get(ch, 0) + 1 for i, ch in enumerate(s): if count[ch] == 1: return i return -1第二版加了类型判断和超长输入限制。你可能觉得LeetCode标准答案不会这么写,但在安全公司,这种“不能盲目信任输入”的习惯是加分项。尤其是后端接口的入参可能来自前端、第三方服务、甚至攻击流量,写接口时先想边界,能少出很多事故。
4.2 边界条件和测试用例,是安全岗笔试的隐形题
编程题里最容易被忽略的是边界条件。我后来和面试官复盘时,他说批卷子时最怕看到“注释里写着:一般不会发生这种情况”——因为安全工程师的日常,就是处理那些“不太可能发生”的情况。
这道题我会在提交前先自测几组用例:
- 输入:
"",输出:-1。 - 输入:
"abcab",输出:下标2,对应字符c。 - 输入:
"abab",输出:-1。 - 输入:
None或非字符串对象,输出:-1。 - 输入包含中文或emoji,要确认字符遍历方式不会把Unicode拆错。
如果是线上笔试系统只允许提交代码,我会在代码开头用注释把这组用例写上去,让阅卷人看到我的思考过程。这不算“作弊”,这是在展示测试设计能力。安全岗写代码,至少要像写测试用例一样认真。
4.3 时间分配:“先把测试用例写出来,再动手写代码”
我习惯的编程题流程是:拿到题目后,先不要急着写代码,用3分钟在纸上把边界用例列出来,再写主逻辑,最后留2分钟跑一遍用例。这种习惯在LeetCode上可能显得慢,但在笔试考场上反而稳。因为你一旦把“空字符串、单个字符、全部重复、超长输入、非字符串”这些情况想清楚,代码框架就已经出来了。
如果题目没有明说“是否区分大小写”“下标从0开始还是从1开始”,我会在注释里写清楚自己的假设。安全岗笔试很看中这种“需求说不清,我就去确认”的沟通意识,而不是闷头实现一个和出题人预期不一致的版本。
5. 考后复盘:一张“原理-利用-修复”对照表带来面试机会
笔试交卷后,不管感觉好不好,我建议立刻做一件事:把卷子里见过的考点整理成一张“原理-利用-修复”对照表。这套复盘方法,我到现在都还在用。它不是简单抄笔记,而是逼你把每一个漏洞都切成三段来理解。
5.1 我是怎么整理那张表的
找一张A4纸,横轴写三个字段:原理、利用、修复。纵轴按知识点一行一行写。最开始几行可能会很粗糙,比如“SQL注入:原理是SQL拼接;利用是注入参数;修复是用预编译”。但整理到一半,你会发现自己能填出来的部分越来越少,而那些填不出来的,恰恰就是你的复习重点。
我当时花了两个晚上,把路径遍历、SQL注入、XSS、CSRF、SSRF、不安全反序列化、验证码绕过这些考点全部过了一遍。这里给出一个简化版,大家可以直接做参考:
| 考点 | 一句话原理 | 常见利用入口 | 笔试高频问法 | 最稳修复关键词 |
|---|---|---|---|---|
| SQL注入 | 未参数化的SQL拼接 | 登录、查询、排序参数 | 写修复代码 | 参数化查询 / PreparedStatement、最小权限 |
| XSS | 未转义输出用户输入 | 搜索框、昵称、评论 | 反射型/存储型区别 | 上下文感知编码、CSP、HttpOnly |
| CSRF | 浏览器自动携带Cookie | 敏感操作接口 | 如何防御 | CSRF Token、SameSite、二次校验 |
| 路径遍历 | 路径拼接可跳出目录 | 下载、导入导出接口 | 找出漏洞并修复 | normalize + 白名单 + startsWith |
| 不安全反序列化 | 不可信数据被还原为对象 | Java/PHP反序列化入口 | 如何识别并加固 | 类型白名单、签名校验 |
这张表整理完后,我对后续面试的底气提升非常明显。面试官如果问“你对Web安全怎么看”,我不会傻背定义,而是直接说:“我习惯把每个漏洞拆成原理、利用、修复三条线来讲,比如路径遍历……”对方马上会想跟你有更多交流。
5.2 笔试没考好怎么办:我后来总结的三个“扳回”动作
并不是所有笔试分数高的人都能走到最后,也不是笔试一般的人就没有机会。我身边有同学笔试时编程题没AC,但简历里有CTF经历,照样直接进面试。如果你对笔试结果不自信,可以试试这三个动作。
第一,24小时内复盘错题,把修正后的思路写成一篇技术笔记,发布到自己的博客或私有仓库里。面试时如果被问到相关知识点,你可以坦然说“这道题笔试时我没答好,但后续我重新梳理了一遍”,这反而是加分项。
第二,把简历上跟安全相关的项目描述再打磨一下,加一句“对用户输入做白名单校验”“对文件下载接口做路径归一化”这类具体动作。但注意不要造假,你做过的项目、用过的技术栈都要经得起深挖。
第三,面试时主动要求讲一道你在笔试复盘里印象最深的题。安全岗面试非常看重主动思考的能力,你能把路径遍历的完整思路讲清楚,比背十道概念题都管用。
5.3 给下一届校招生的安全岗笔试速查清单
最后给一份速查清单,不保证覆盖所有公司,但至少能把常见考点兜住:
- HTTP基础:GET/POST/PUT/DELETE语义,状态码401/403/404/500区别,Cookie与Session关系。
- Web漏洞:SQL注入、XSS、CSRF、SSRF、路径遍历、反序列化,每一类都得能说出攻击链路和修复链路。
- 密码学:RSA加解密基本公式,对称与非对称区别,ECB模式问题,密码存储要用加盐慢哈希。
- 编程能力:字符串/数组高频题,注意类型、空值、超长、Unicode边界。
- 系统基础:Linux常用命令、文件权限、日志位置,安全岗不会只考Web,基础系统知识也要扫一遍。
我记得当年交卷前,把路径遍历那道题的修复代码反复改了三遍。后来真的开始做代码审计才发现,笔试里那道题其实就是日常工作的最小副本:一段看似人畜无害的文件下载代码,因为少了normalize和startsWith校验,变成了一个可以读取任意文件的突破口。如果你正在准备奇安信或者同类安全公司的校招,别把笔试当成一场考试,把它当成一次模拟工作台。你在卷子上写的每一行修复代码,都会是你以后写给自己的作品。