本篇包含cicd-goat靶场难度为难的题目解题记录,难度为简单、中等的解题记录见上一篇文章

Hard

Hearts

这个题目需要获取存储在Jenkins上的系统凭证(Linux服务器的密码),按题解的思路是通过爆破特权用户的口令,登陆后新建主机然后截取凭证信息拿密码。

在Jenkins用户界面可以看到knave用户是Agents admin用户:
knave

可以对这个用户进行爆破,最终得到的密码是rockme。我自己测试并没有爆破出来,爆破是一门手艺,关键在于合适的字典收集与选择🤷‍♂️,需要经验的积累。

那么怎么设置jenkins可以防止系统被爆破了?

我简单搜索了下,并没有比较简单的解决方案。

  1. 首先在Jenkins系统设置层面,并没有关闭/启用验证码、账号锁定/自动解锁的设置选项(以cicd-goat中Jenkins 2.332.1为例,不确定后续版本是否会增加);
  2. Jenkins支持的认证方式中Jenkins自有用户数据库、LDAP等方式认证,按道理可以使用AD域或者LDAP的账号锁定设置来进行,但如果因为攻击者爆破导致域账号被锁定也很烦;
  3. Jenkins插件市场里的MFA/Two-Factor-Authentication(2FA)或许能满足要求,但是短信OTP/手机应用OTP验证,需要切换到付费版本。
    plugins

因此,防护Jenkins最好还是从网络上进行白名单访问管控+用户密码强口令

言归正传,登录knave用户之后,可以新建构建运行节点:
add node

配置直接拷贝agent1的即可,需要注意的是,需要将SSH配置指向蜜罐地址和端口:
login into honeybot

在Github上可以搜到一些SSH蜜罐,我自己实际测试下来:

  1. ssh-honeypot(634个star)不太行,使用时遇到主机Key验证的问题,而且agent实际连的时候一直提示连不上、看不到用户提交的密码;
  2. sshesame(1.6k个star)用起来很好,一次就成功了。

得到的SSH密码即是flag7:
ssh password

Dormouse

这个题目又是一个类似于供应链被攻击导致项目流水线一同被攻击的场景。

dormouse项目从prod服务器下载reportcov.sh脚本,并运行:
shell script

脚本内容为:

> curl http://0177.0.0.01:8008/reportcov.sh
# Reportcov is maintained at http://localhost:3000/Cov/reportcov
curl -F "data=@tests/index.html" "http://localhost:1111/upload" -H "Authorization: Token ${TOKEN}"

reportcov项目的Jenkinsfile文件中包含很经典的命令注入漏洞:双引号括起来的变量会导致命令注入,而单引号括起来会被当成字符串,不会引起命令注入。
reportcov jenkinsfile
quotes

至此,我们的目的很明确:

  1. 在reportcov项目提交恶意PR,通过标题的命令注入,修改reportcov.sh文件内容;
  2. 运行Jenkins中dormouse项目的流水线,拿去flag。

在本地新建一个脚本,并托管在本机之后:

echo "${KEY}" > /tmp/1key && chmod 400 /tmp/1key
echo "env|base64" > /tmp/reportcov.sh
scp -o StrictHostKeyChecking=no -i /tmp/1key /tmp/reportcov.sh root@prod:/var/www/localhost/htdocs

随后提交PR,并在标题中进行命令注入,重新运行Jenkins流水线任务,即可得到flag信息:
PR
get flag

事实上,我花了很长时间来测试通过标题实现命令注入,起初我一直将要注入的命令括在反引号中间、都没成功,搞得有点怀疑自己。然后我自己想你只会这一招吗?然后换$(...)测试了下,立马就拿到flag了。🤔

2025-01-01 10:26:43补充:
在看了项目在Bsides会议上演讲视频GF - Climbing the Production Mountain: Practical CI/CD Attacks Using CI/CD Goat之后,我有点怀疑针对单双引号括起来的判断,于是使用管理员red_queen账户修改Cov/reportcov的Jenkinsfile,添加了另外3种情况,结果如下:

根据测试结果来看:

  1. 如果在脚本中直接给出命令,那么不论使用双引号还是单引号包裹,命令都会被执行;
  2. 如果使用变量形式注入命令,使用单引号括起来时会进行变量替换、但不会执行命令,而使用双引号括起来时会先执行命令,将命令结果进行替换。在处理字符串中变量时,使用单引号要比双引号安全一点。

Mock Turtle

这一题也很有意思,使用流水线的方式对PR进行检查、自动合并:
jenkinsfile

check1需要使代码变更中,增加的单词数量与减少的单词数量一致:

gitp=`git diff --word-diff=porcelain origin/${CHANGE_TARGET} | grep -e "^+[^+]" | wc -w | xargs`
gitm=`git diff --word-diff=porcelain origin/${CHANGE_TARGET} | grep -e "^-[^-]" | wc -w | xargs`
if [ $(($gitp - $gitm)) -eq 0 ] ; then check1=true; else check1=false; fi

check2对version格式进行校验:

if [ $(wc -l <version) -eq 0 -a $(grep -Po "^\\d{1,2}\\.\\d{1,2}\\.\\d{1,2}$" version) ] ; then check2=true; else check2=false; fi

check3检查是否修改了version文件:

git diff --name-only origin/${CHANGE_TARGET} | grep version)

因此整个利用过程分如下步骤:

  1. 修改jenkinsfile文件中对check1的检验过程,改成gitp与自己相减,同时更新版本号;
  2. 修改jenkinsfile文件,将check1/2/3始终设置为true,同时更新版本号;
  3. 此时提交任何内容均会被合并,假想中的攻击者已取得仓库完整的控制权限。

在提交PR时有一个需要注意的点,需要从Wonderland/mock-turtle的非main分支提交到其main分支,如果直接从fork的repo里提交PR到Wonderland/mock-turtle的main分支时不会流水线动作。
bypass check1

bypass all checkes

此外,最终的目的是取得flag10,这个凭证需要在main分支的流水线中运行取得,直接在PR的流水线中取环境变量并不能拿到flag10。
get flag10

Very Hard

Gryphon

Gryphon是唯一一个跑在Gitlab环境上的场景,难度划分可以定为非常难

环境的架构如下图所示,主要由两个基本不相干的项目组成:

攻击路径按架构图中红色箭头所示,需要:

  1. 构建恶意的PyPI库(pygryphon),通过修改其中关键的hello函数;
  2. wonderland/awesome-app流水线执行时,污染Docker In Docker镜像仓库(python:3.8);
  3. wonderland/nest-of-gold流水线执行时,运行恶意的python3文件,通过OOB方式传递flag信息。

需要注意的是,nest-of-gold的Pipeline文件需要以root用户登录gitlab、进行简单修改,将DOCKER_HOST直接通过环境变量给出:
change pipeline

第一步中pytest测试代码运行过程如下:
pytest

制作恶意的pygryphon包,修改src/pygryphon/greet.py,脚本内容直接来自于丶feng发表在先知社区上的文章:从CICD-GOAT靶场学习top-10-cicd-security-risks
greet.py

删除Gitlab仓库页面packages下的pygryphon包,然后重新打包构建PyPI包,并上传到Gitlab的packages下:

pip install twine build
python3 -m build .
python3 -m twine upload --verbose --skip-existing --config-file ./.pypirc -r gitlab ./dist/*

然后监听写在脚本里的本地的回传地址,等wonderland/awesome-app和wonderland/nest-of-gold流水线运行后,就可以拿到flag信息。

运行pytest -rA test_hello.py时,提交了docker build和push请求:
docker build & push while pytesting

攻击的污染链:

最终成功拿到flag:

疑问:

  1. .gitlab-ci.yml里的image是什么意思?它与DockerfileFROM定义的镜像有何差异?

来自Copilot的解答:
.gitlab-ci.yml中的image:定义了CI/CD管道中作业运行时使用的镜像。
Dockerfile中的FROM:定义了构建容器镜像时的基础镜像。